| // 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 provides utilities for working with Go OSV entries. | |
| // It is separated from package osv because that package | |
| // promises to only import from the standard library. | |
| package osvutils | |
| import ( | |
| "errors" | |
| "fmt" | |
| "strings" | |
| "golang.org/x/vulndb/internal/derrors" | |
| "golang.org/x/vulndb/internal/idstr" | |
| "golang.org/x/vulndb/internal/osv" | |
| "golang.org/x/vulndb/internal/version" | |
| ) | |
| // Validate errors if there are any problems with the OSV Entry. | |
| // It is used to validate OSV entries before publishing them to the | |
| // Go vulnerability database, and has stricter requirements than | |
| // the general OSV format. | |
| func Validate(e *osv.Entry) (err error) { | |
| derrors.Wrap(&err, "Validate(%s)", e.ID) | |
| return validate(e, true) | |
| } | |
| // ValidateExceptTimestamps errors if there are any problems with the | |
| // OSV Entry, with the exception of the timestamps (published, modified and | |
| // withdrawn) which are not checked. | |
| // This is used to validate entries at CL submit time, before their timestamps | |
| // are corrected. | |
| func ValidateExceptTimestamps(e *osv.Entry) (err error) { | |
| derrors.Wrap(&err, "ValidateExceptTimestamps(%s)", e.ID) | |
| return validate(e, false) | |
| } | |
| var ( | |
| // Errors for incorrect timestamps. | |
| errNoModified = errors.New("modified time must be non-zero") | |
| errNoPublished = errors.New("published time must be non-zero") | |
| errPublishedAfterModified = errors.New("published time cannot be after modified time") | |
| // Errors for missing fields. | |
| errNoID = errors.New("id field is empty") | |
| errNoSchemaVersion = errors.New("schema_version field is empty") | |
| errNoSummary = errors.New("summary field is empty") | |
| errNoDetails = errors.New("details field is empty") | |
| errNoAffected = errors.New("affected field is empty") | |
| errNoDatabaseSpecific = errors.New("database_specific field is empty") | |
| errNoModule = errors.New("affected field missing module path") | |
| errNotGoEcosystem = errors.New("affected ecosystem is not Go") | |
| errNoRanges = errors.New("affected field contains no ranges") | |
| errNoEcosystemSpecific = errors.New("affected field contains no ecosystem_specific field") | |
| errNoPackagePath = errors.New("affected.ecosystem_specific.imports field has no package path") | |
| // Errors for invalid fields. | |
| errInvalidAlias = errors.New("alias must be CVE or GHSA ID") | |
| errInvalidPkgsiteURL = errors.New("database_specific.URL must be a link to https://pkg.go.dev/vuln/<Go id>") | |
| errInvalidPackagePath = errors.New("package path must be prefixed by module path") | |
| errTooManyRanges = errors.New("each module should have exactly one version range") | |
| errRangeTypeNotSemver = errors.New("range type must be SEMVER") | |
| errNoRangeEvents = errors.New("range must contain one or more events") | |
| errOutOfOrderRange = errors.New("introduced and fixed versions must alternate") | |
| errUnsortedRange = errors.New("range events must be in strictly ascending order") | |
| errNoIntroducedOrFixed = errors.New("introduced or fixed must be set") | |
| errBothIntroducedAndFixed = errors.New("introduced and fixed cannot both be set in same event") | |
| errInvalidSemver = errors.New("invalid or non-canonical semver version") | |
| ) | |
| func validate(e *osv.Entry, checkTimestamps bool) (err error) { | |
| if checkTimestamps { | |
| switch { | |
| case e.Modified.IsZero(): | |
| return errNoModified | |
| case e.Published.IsZero(): | |
| return errNoPublished | |
| case e.Published.After(e.Modified.Time): | |
| return fmt.Errorf("%w (published=%s, modified=%s)", errPublishedAfterModified, e.Published, e.Modified) | |
| } | |
| } | |
| // Check for missing required fields. | |
| switch { | |
| case e.ID == "": | |
| return errNoID | |
| case e.SchemaVersion == "": | |
| return errNoSchemaVersion | |
| case e.Summary == "": | |
| return errNoSummary | |
| case e.Details == "" && !hasAdvisory(e): | |
| return errNoDetails | |
| case len(e.Affected) == 0: | |
| return errNoAffected | |
| case e.DatabaseSpecific == nil: | |
| return errNoDatabaseSpecific | |
| } | |
| for _, a := range e.Affected { | |
| if err := validateAffected(&a); err != nil { | |
| return err | |
| } | |
| } | |
| for _, alias := range e.Aliases { | |
| if !idstr.IsAliasType(alias) { | |
| return fmt.Errorf("%w (found alias %s)", errInvalidAlias, alias) | |
| } | |
| } | |
| return validateDatabaseSpecific(e.DatabaseSpecific) | |
| } | |
| func hasAdvisory(entry *osv.Entry) bool { | |
| for _, ref := range entry.References { | |
| if ref.Type == osv.ReferenceTypeAdvisory { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func validateAffected(a *osv.Affected) error { | |
| switch { | |
| case a.Module.Path == "": | |
| return errNoModule | |
| case a.Module.Ecosystem != osv.GoEcosystem: | |
| return errNotGoEcosystem | |
| } | |
| if err := ValidateRanges(a.Ranges); err != nil { | |
| return err | |
| } | |
| return validateEcosystemSpecific(a.EcosystemSpecific, a.Module.Path) | |
| } | |
| func ValidateRanges(ranges []osv.Range) error { | |
| switch { | |
| case len(ranges) == 0: | |
| return errNoRanges | |
| case len(ranges) > 1: | |
| return fmt.Errorf("%w (found %d ranges)", errTooManyRanges, len(ranges)) | |
| } | |
| return validateRange(&ranges[0]) | |
| } | |
| func validateRange(r *osv.Range) error { | |
| switch { | |
| case r.Type != osv.RangeTypeSemver: | |
| return fmt.Errorf("%w (found range type %q)", | |
| errRangeTypeNotSemver, r.Type) | |
| case len(r.Events) == 0: | |
| return errNoRangeEvents | |
| } | |
| // Check that all the events are valid and sorted in ascending order. | |
| prev, err := parseRangeEvent(&r.Events[0]) | |
| if err != nil { | |
| return err | |
| } | |
| for _, event := range r.Events[1:] { | |
| current, err := parseRangeEvent(&event) | |
| if err != nil { | |
| return fmt.Errorf("invalid range event: %w", err) | |
| } | |
| // Introduced and fixed versions must alternate. | |
| if current.introduced == prev.introduced { | |
| return errOutOfOrderRange | |
| } | |
| if !less(prev.v, current.v) { | |
| return fmt.Errorf("%w (found %s>=%s)", errUnsortedRange, prev.v, current.v) | |
| } | |
| prev = current | |
| } | |
| return nil | |
| } | |
| func less(v, w string) bool { | |
| // Ensure that version 0 is always lowest. | |
| if v == "0" { | |
| return true | |
| } | |
| if w == "0" { | |
| return false | |
| } | |
| return version.Before(v, w) | |
| } | |
| type event struct { | |
| v string | |
| introduced bool | |
| } | |
| func parseRangeEvent(e *osv.RangeEvent) (*event, error) { | |
| introduced, fixed := e.Introduced, e.Fixed | |
| var v string | |
| var isIntroduced bool | |
| switch { | |
| case introduced == "" && fixed == "": | |
| return nil, errNoIntroducedOrFixed | |
| case introduced != "" && fixed != "": | |
| return nil, errBothIntroducedAndFixed | |
| case introduced == "0": | |
| return &event{v: "0", introduced: true}, nil | |
| case introduced != "": | |
| v = introduced | |
| isIntroduced = true | |
| case fixed != "": | |
| v = fixed | |
| isIntroduced = false | |
| } | |
| if !version.IsValid(v) || v != version.Canonical(v) { | |
| return nil, fmt.Errorf("%w (found %s)", errInvalidSemver, v) | |
| } | |
| return &event{v: v, introduced: isIntroduced}, nil | |
| } | |
| func validateEcosystemSpecific(es *osv.EcosystemSpecific, module string) error { | |
| if es == nil { | |
| return errNoEcosystemSpecific | |
| } | |
| for _, pkg := range es.Packages { | |
| if pkg.Path == "" { | |
| return errNoPackagePath | |
| } | |
| // Package path must be prefixed by module path unless it is | |
| // in the Go standard library or toolchain. | |
| if (module != osv.GoStdModulePath && module != osv.GoCmdModulePath) && | |
| !strings.HasPrefix(pkg.Path, module) { | |
| return fmt.Errorf("%w (found module=%q, package=%q)", errInvalidPackagePath, module, pkg.Path) | |
| } | |
| } | |
| return nil | |
| } | |
| func validateDatabaseSpecific(d *osv.DatabaseSpecific) error { | |
| if !idstr.IsGoAdvisory(d.URL) { | |
| return fmt.Errorf("%w (found URL %q)", errInvalidPkgsiteURL, d.URL) | |
| } | |
| return nil | |
| } |