| // 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" |
| "regexp" |
| "strings" |
| |
| "golang.org/x/vulndb/internal/cveschema5" |
| "golang.org/x/vulndb/internal/derrors" |
| "golang.org/x/vulndb/internal/ghsa" |
| "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") |
| errNoReferences = errors.New("references 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") |
| |
| pkgsiteLinkRegex = regexp.MustCompile(`^https://pkg.go.dev/vuln/GO-\d{4}-\d{4,}$`) |
| ) |
| |
| 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 len(e.References) == 0: |
| return errNoReferences |
| 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 !ghsa.IsGHSA(alias) && !cveschema5.IsCVE(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 !pkgsiteLinkRegex.MatchString(d.URL) { |
| return fmt.Errorf("%w (found URL %q)", errInvalidPkgsiteURL, d.URL) |
| } |
| return nil |
| } |