| // Copyright 2023 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 osvutils | |
| import ( | |
| "errors" | |
| "testing" | |
| "time" | |
| "golang.org/x/vulndb/internal/osv" | |
| ) | |
| var ( | |
| jan1999 = osv.Time{Time: time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC)} | |
| jan2000 = osv.Time{Time: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)} | |
| ) | |
| // testEntry creates a valid Entry and modifies it by running transform. | |
| // If transform is nil, it returns the base test Entry. | |
| func testEntry(transform func(e *osv.Entry)) *osv.Entry { | |
| e := &osv.Entry{ | |
| SchemaVersion: "1.3.1", | |
| ID: "GO-1999-0001", | |
| Published: jan1999, | |
| Modified: jan2000, | |
| Aliases: []string{"CVE-1999-1111"}, | |
| Summary: "A summary", | |
| Details: "Some details", | |
| Affected: []osv.Affected{ | |
| { | |
| Module: osv.Module{ | |
| Path: "example.com/module", | |
| Ecosystem: "Go", | |
| }, | |
| Ranges: []osv.Range{ | |
| { | |
| Type: "SEMVER", | |
| Events: []osv.RangeEvent{ | |
| {Introduced: "0"}, {Fixed: "1.1.0"}, | |
| {Introduced: "1.2.0"}, | |
| {Fixed: "1.2.2"}, | |
| }}}, | |
| EcosystemSpecific: &osv.EcosystemSpecific{ | |
| Packages: []osv.Package{{Path: "example.com/module/package", Symbols: []string{"Symbol"}}}}}, | |
| { | |
| Module: osv.Module{ | |
| Path: "stdlib", | |
| Ecosystem: "Go", | |
| }, | |
| Ranges: []osv.Range{ | |
| { | |
| Type: "SEMVER", | |
| Events: []osv.RangeEvent{ | |
| {Introduced: "0"}, {Fixed: "1.1.0"}, | |
| {Introduced: "1.2.0"}, | |
| {Fixed: "1.2.2"}, | |
| }}}, | |
| EcosystemSpecific: &osv.EcosystemSpecific{ | |
| Packages: []osv.Package{{Path: "package", Symbols: []string{"Symbol"}}}}}, | |
| }, | |
| References: []osv.Reference{ | |
| {Type: "FIX", URL: "https://example.com/cl/123"}, | |
| }, | |
| DatabaseSpecific: &osv.DatabaseSpecific{ | |
| URL: "https://pkg.go.dev/vuln/GO-1999-0001"}} | |
| if transform == nil { | |
| return e | |
| } | |
| transform(e) | |
| return e | |
| } | |
| func TestValidate(t *testing.T) { | |
| t.Run("ok", func(t *testing.T) { | |
| if err := Validate(testEntry(nil)); err != nil { | |
| t.Error(err) | |
| } | |
| if err := ValidateExceptTimestamps(testEntry(nil)); err != nil { | |
| t.Error("ValidateExceptTimestamps():", err) | |
| } | |
| }) | |
| t.Run("timestamps", func(t *testing.T) { | |
| for _, tc := range []struct { | |
| name string | |
| entry *osv.Entry | |
| wantErr error | |
| }{ | |
| { | |
| name: "no modified", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Modified = osv.Time{} | |
| }), | |
| wantErr: errNoModified, | |
| }, | |
| { | |
| name: "no published", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Published = osv.Time{} | |
| }), | |
| wantErr: errNoPublished, | |
| }, | |
| { | |
| name: "published after modified", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Modified = jan1999 | |
| e.Published = jan2000 | |
| }), | |
| wantErr: errPublishedAfterModified, | |
| }, | |
| } { | |
| t.Run(tc.name, func(t *testing.T) { | |
| want := tc.wantErr | |
| if got := Validate(tc.entry); !errors.Is(got, want) { | |
| t.Errorf("Validate() error = %v, want error %v", got, want) | |
| } | |
| // There should be no error when we don't check timestamps. | |
| if err := ValidateExceptTimestamps(tc.entry); err != nil { | |
| t.Errorf("ValidateExceptTimestamps() error = %v", err) | |
| } | |
| }) | |
| } | |
| }) | |
| t.Run("fail", func(t *testing.T) { | |
| for _, tc := range []struct { | |
| name string | |
| entry *osv.Entry | |
| wantErr error | |
| }{ | |
| { | |
| name: "no ID", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.ID = "" | |
| }), | |
| wantErr: errNoID, | |
| }, | |
| { | |
| name: "no schema version", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.SchemaVersion = "" | |
| }), | |
| wantErr: errNoSchemaVersion, | |
| }, | |
| { | |
| name: "no summary", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Summary = "" | |
| }), | |
| wantErr: errNoSummary, | |
| }, | |
| { | |
| name: "no details", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Details = "" | |
| }), | |
| wantErr: errNoDetails, | |
| }, | |
| { | |
| name: "no affected", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected = nil | |
| }), | |
| wantErr: errNoAffected, | |
| }, | |
| { | |
| name: "no database specific", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.DatabaseSpecific = nil | |
| }), | |
| wantErr: errNoDatabaseSpecific, | |
| }, | |
| { | |
| name: "missing module path", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Module.Path = "" | |
| }), | |
| wantErr: errNoModule, | |
| }, | |
| { | |
| name: "non-Go ecosystem", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Module.Ecosystem = "Goo" | |
| }), | |
| wantErr: errNotGoEcosystem, | |
| }, | |
| { | |
| name: "no version ranges", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges = nil | |
| }), | |
| wantErr: errNoRanges, | |
| }, | |
| { | |
| name: "no ecosystem specific", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].EcosystemSpecific = nil | |
| }), | |
| wantErr: errNoEcosystemSpecific, | |
| }, | |
| { | |
| name: "no package path", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].EcosystemSpecific.Packages[0].Path = "" | |
| }), | |
| wantErr: errNoPackagePath, | |
| }, | |
| { | |
| name: "invalid alias", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Aliases = append(e.Aliases, "CVE-GHSA-123") | |
| }), | |
| wantErr: errInvalidAlias, | |
| }, | |
| { | |
| name: "invalid pkgsite URL", | |
| entry: testEntry(func(e *osv.Entry) { | |
| // missing "/vuln/" | |
| e.DatabaseSpecific.URL = "https://pkg.go.dev/GO-1234-5667" | |
| }), | |
| wantErr: errInvalidPkgsiteURL, | |
| }, | |
| { | |
| name: "package path not prefixed by module path", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Module.Path = "example.com/module" | |
| e.Affected[0].EcosystemSpecific.Packages[0].Path = "example.com/package" | |
| }), | |
| wantErr: errInvalidPackagePath, | |
| }, | |
| { | |
| name: "more than one version range", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges = append(e.Affected[0].Ranges, osv.Range{}) | |
| }), | |
| wantErr: errTooManyRanges, | |
| }, | |
| { | |
| name: "non-SEMVER range", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Type = "unknown" | |
| }), | |
| wantErr: errRangeTypeNotSemver, | |
| }, | |
| { | |
| name: "no range events", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events = nil | |
| }), | |
| wantErr: errNoRangeEvents, | |
| }, | |
| { | |
| name: "out of order range", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events = []osv.RangeEvent{ | |
| {Fixed: "1.1.1"}, {Fixed: "1.1.2"}, | |
| } | |
| }), | |
| wantErr: errOutOfOrderRange, | |
| }, | |
| { | |
| name: "unsorted range", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events = []osv.RangeEvent{ | |
| {Fixed: "1.1.1"}, {Introduced: "1.1.0"}, | |
| } | |
| }), | |
| wantErr: errUnsortedRange, | |
| }, | |
| { | |
| name: "no introduced or fixed in range event", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events[0] = osv.RangeEvent{} | |
| }), | |
| wantErr: errNoIntroducedOrFixed, | |
| }, | |
| { | |
| name: "both introduced and fixed in range event", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events = []osv.RangeEvent{ | |
| {Introduced: "1.1.0", Fixed: "1.1.1"}, | |
| } | |
| }), | |
| wantErr: errBothIntroducedAndFixed, | |
| }, | |
| { | |
| name: "non-canonical semver", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events = []osv.RangeEvent{ | |
| {Introduced: "1.1"}, | |
| } | |
| }), | |
| wantErr: errInvalidSemver, | |
| }, | |
| { | |
| name: "invalid semver", | |
| entry: testEntry(func(e *osv.Entry) { | |
| e.Affected[0].Ranges[0].Events = []osv.RangeEvent{ | |
| {Introduced: "1x2x3"}, | |
| } | |
| }), | |
| wantErr: errInvalidSemver, | |
| }, | |
| } { | |
| t.Run(tc.name, func(t *testing.T) { | |
| want := tc.wantErr | |
| if got := Validate(tc.entry); !errors.Is(got, want) { | |
| t.Errorf("Validate() error = %v, want error %v", got, want) | |
| } | |
| if got := ValidateExceptTimestamps(tc.entry); !errors.Is(got, want) { | |
| t.Errorf("ValidateExceptTimestamps() error = %v, want error %v", got, want) | |
| } | |
| }) | |
| } | |
| }) | |
| } |