зеркало из https://github.com/golang/vulndb.git
1032 строки
25 KiB
Go
1032 строки
25 KiB
Go
// 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 report
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/mod/module"
|
|
"golang.org/x/vulndb/internal/idstr"
|
|
"golang.org/x/vulndb/internal/osv"
|
|
"golang.org/x/vulndb/internal/osvutils"
|
|
"golang.org/x/vulndb/internal/proxy"
|
|
"golang.org/x/vulndb/internal/stdlib"
|
|
"golang.org/x/vulndb/internal/version"
|
|
)
|
|
|
|
func (r *Report) Fix(pc *proxy.Client) {
|
|
r.deleteNotes(NoteTypeFix)
|
|
expandGitCommits(r)
|
|
_ = r.FixModules(pc)
|
|
r.FixText()
|
|
r.FixReferences()
|
|
}
|
|
|
|
func (r *Report) FixText() {
|
|
fixLines := func(sp *string) {
|
|
*sp = fixLineLength(*sp, maxLineLength)
|
|
}
|
|
fixLines((*string)(&r.Summary))
|
|
fixLines((*string)(&r.Description))
|
|
if r.CVEMetadata != nil {
|
|
fixLines(&r.CVEMetadata.Description)
|
|
}
|
|
|
|
r.fixSummary()
|
|
}
|
|
|
|
func (r *Report) fixSummary() {
|
|
summary := r.Summary.String()
|
|
|
|
// If there is no summary, create a basic one.
|
|
if summary == "" {
|
|
if aliases := r.Aliases(); len(aliases) != 0 {
|
|
summary = aliases[0]
|
|
} else {
|
|
summary = "Vulnerability"
|
|
}
|
|
}
|
|
|
|
// Add a path if one exists and is needed.
|
|
if paths := r.nonStdPaths(); len(paths) > 0 && !containsPath(summary, paths) {
|
|
summary = fmt.Sprintf("%s in %s", summary, stripMajor(paths[0]))
|
|
}
|
|
|
|
r.Summary = Summary(fixSpelling(summary))
|
|
}
|
|
|
|
func stripMajor(path string) string {
|
|
base, _, ok := module.SplitPathVersion(path)
|
|
if !ok {
|
|
return path
|
|
}
|
|
return base
|
|
}
|
|
|
|
func (v *Version) commitHashToVersion(modulePath string, pc *proxy.Client) {
|
|
if v == nil {
|
|
return
|
|
}
|
|
|
|
vv := v.Version
|
|
if version.IsCommitHash(vv) {
|
|
if c, err := pc.CanonicalModuleVersion(modulePath, vv); err == nil { // no error
|
|
v.Version = c
|
|
}
|
|
}
|
|
}
|
|
|
|
// FixVersions replaces each version with its canonical form (if possible),
|
|
// sorts version ranges, and moves versions to their proper spot.
|
|
func (m *Module) FixVersions(pc *proxy.Client) {
|
|
for _, v := range m.Versions {
|
|
v.commitHashToVersion(m.Module, pc)
|
|
}
|
|
m.VulnerableAt.commitHashToVersion(m.Module, pc)
|
|
|
|
m.Versions.fix()
|
|
m.UnsupportedVersions.fix()
|
|
m.VulnerableAt.fix()
|
|
|
|
if pc != nil && !m.IsFirstParty() {
|
|
found, notFound, _ := m.classifyVersions(pc)
|
|
if len(notFound) != 0 {
|
|
m.Versions = found
|
|
m.NonGoVersions = append(m.NonGoVersions, notFound...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Version) fix() {
|
|
if v == nil {
|
|
return
|
|
}
|
|
vv := version.TrimPrefix(v.Version)
|
|
if version.IsValid(vv) {
|
|
vv = version.Canonical(vv)
|
|
}
|
|
v.Version = vv
|
|
}
|
|
|
|
func (vs *Versions) fix() {
|
|
for i := range *vs {
|
|
(*vs)[i].fix()
|
|
}
|
|
sort.SliceStable(*vs, func(i, j int) bool {
|
|
return version.Before((*vs)[i].Version, (*vs)[j].Version)
|
|
})
|
|
// Remove duplicates.
|
|
*vs = slices.Compact(*vs)
|
|
*vs = slices.CompactFunc(*vs, func(a, b *Version) bool {
|
|
return a.Type == b.Type && a.Version == b.Version
|
|
})
|
|
}
|
|
|
|
func (m *Module) fixVulnerableAt(pc *proxy.Client) error {
|
|
if m.VulnerableAt != nil {
|
|
return nil
|
|
}
|
|
if m.IsFirstParty() {
|
|
return fmt.Errorf("not implemented for std/cmd")
|
|
}
|
|
// Don't attempt to guess if the given version ranges don't make sense.
|
|
if err := m.checkModVersions(pc); err != nil {
|
|
return err
|
|
}
|
|
v, err := m.guessVulnerableAt(pc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.VulnerableAt = VulnerableAt(v)
|
|
return nil
|
|
}
|
|
|
|
var errZeroPseudo = errors.New("cannot auto-guess when fixed version is 0.0.0 pseudo-version")
|
|
|
|
// Find the latest fixed and introduced version, assuming the version
|
|
// ranges are sorted and valid.
|
|
func (vs Versions) latestVersions() (introduced, fixed *Version) {
|
|
if len(vs) == 0 {
|
|
return
|
|
}
|
|
last := vs[len(vs)-1]
|
|
if last.IsIntroduced() {
|
|
introduced = last
|
|
return
|
|
}
|
|
fixed = last
|
|
if len(vs) > 1 {
|
|
if penultimate := vs[len(vs)-2]; penultimate.IsIntroduced() {
|
|
introduced = penultimate
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// guessVulnerableAt attempts to find a vulnerable_at
|
|
// version using the module proxy, assuming that the version ranges
|
|
// have already been validated.
|
|
// If there is no fix, the latest version is used.
|
|
func (m *Module) guessVulnerableAt(pc *proxy.Client) (v string, err error) {
|
|
if m.IsFirstParty() {
|
|
return "", errors.New("cannot auto-guess vulnerable_at for first-party modules")
|
|
}
|
|
|
|
introduced, fixed := m.Versions.latestVersions()
|
|
|
|
// If there is no latest fix, find the latest version of the module.
|
|
if fixed == nil {
|
|
latest, err := pc.Latest(m.Module)
|
|
if err != nil || latest == "" {
|
|
return "", fmt.Errorf("no fix, but could not find latest version from proxy: %s", err)
|
|
}
|
|
if introduced != nil && version.Before(latest, introduced.Version) {
|
|
return "", fmt.Errorf("latest version (%s) is before last introduced version", latest)
|
|
}
|
|
return latest, nil
|
|
}
|
|
|
|
// If the latest fixed version is a 0.0.0 pseudo-version, or not a valid version,
|
|
// don't attempt to determine the vulnerable_at version.
|
|
if !version.IsValid(fixed.Version) {
|
|
return "", errors.New("cannot auto-guess when fixed version is invalid")
|
|
}
|
|
if strings.HasPrefix(fixed.Version, "0.0.0-") {
|
|
return "", errZeroPseudo
|
|
}
|
|
|
|
// Otherwise, find the version right before the fixed version.
|
|
vs, err := pc.Versions(m.Module)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not find versions from proxy: %s", err)
|
|
}
|
|
for i := len(vs) - 1; i >= 0; i-- {
|
|
if version.Before(vs[i], fixed.Version) {
|
|
// Make sure the version is >= the latest introduced version.
|
|
if introduced == nil || !version.Before(vs[i], introduced.Version) {
|
|
return vs[i], nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", errors.New("could not find tagged version between introduced and fixed")
|
|
}
|
|
|
|
// fixLineLength returns a copy of s with all lines trimmed to <=n characters
|
|
// (with the exception of single-word lines).
|
|
// It preserves paragraph breaks (indicated by "\n\n") and markdown-style list
|
|
// breaks.
|
|
func fixLineLength(s string, n int) string {
|
|
var result strings.Builder
|
|
result.Grow(len(s))
|
|
for i, paragraph := range strings.Split(toParagraphs(s), "\n\n") {
|
|
if i > 0 {
|
|
result.WriteString("\n\n")
|
|
}
|
|
var lines []string
|
|
for _, forcedLine := range strings.Split(paragraph, "\n") {
|
|
words := strings.Split(forcedLine, " ")
|
|
start, length := 0, 0
|
|
for k, word := range words {
|
|
newLength := length + len(word)
|
|
if length > 0 {
|
|
newLength++ // space character
|
|
}
|
|
if newLength <= n {
|
|
length = newLength
|
|
continue
|
|
}
|
|
// Adding the word would put the line over the max length,
|
|
// so add the line as is (if it is non-empty).
|
|
if length > 0 {
|
|
lines = append(lines, strings.Join(words[start:k], " "))
|
|
}
|
|
// Begin a new line with just the word.
|
|
start, length = k, len(word)
|
|
}
|
|
// Add the last line.
|
|
if length > 0 {
|
|
lines = append(lines, strings.Join(words[start:], " "))
|
|
}
|
|
}
|
|
result.WriteString(strings.Join(lines, "\n"))
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
var urlReplacements = []struct {
|
|
re *regexp.Regexp
|
|
repl string
|
|
}{{
|
|
regexp.MustCompile(`golang.org`),
|
|
`go.dev`,
|
|
}, {
|
|
regexp.MustCompile(`https?://groups.google.com/forum/\#\![^/]*/([^/]+)/([^/]+)/(.*)`),
|
|
|
|
`https://groups.google.com/g/$1/c/$2/m/$3`,
|
|
}, {
|
|
regexp.MustCompile(`.*github.com/golang/go/issues`),
|
|
`https://go.dev/issue`,
|
|
}, {
|
|
regexp.MustCompile(`.*github.com/golang/go/commit`),
|
|
`https://go.googlesource.com/+`,
|
|
},
|
|
}
|
|
|
|
func fixURL(u string) string {
|
|
for _, repl := range urlReplacements {
|
|
u = repl.re.ReplaceAllString(u, repl.repl)
|
|
}
|
|
return u
|
|
}
|
|
|
|
func (r *Report) FixModules(pc *proxy.Client) (errs error) {
|
|
var fixed []*Module
|
|
for _, m := range r.Modules {
|
|
m.Module = transform(m.Module)
|
|
extractImportPath(m, pc)
|
|
fixed = append(fixed, m.splitByMajor(pc)...)
|
|
}
|
|
r.Modules = fixed
|
|
|
|
merged, err := merge(fixed)
|
|
if err != nil {
|
|
r.AddNote(NoteTypeFix, "module merge error: %s", err)
|
|
errs = errors.Join(errs, err)
|
|
} else {
|
|
r.Modules = merged
|
|
}
|
|
|
|
// For unreviewed reports, assume that all major versions
|
|
// up to the highest mentioned are affected at all versions.
|
|
if r.IsUnreviewed() {
|
|
r.addMissingMajors(pc)
|
|
}
|
|
|
|
// Fix the versions *after* the modules have been merged.
|
|
for _, m := range r.Modules {
|
|
m.FixVersions(pc)
|
|
if err := m.fixVulnerableAt(pc); err != nil {
|
|
r.AddNote(NoteTypeFix, "%s: could not add vulnerable_at: %v", m.Module, err)
|
|
errs = errors.Join(errs, err)
|
|
}
|
|
}
|
|
|
|
sortModules(r.Modules)
|
|
return errs
|
|
}
|
|
|
|
// extractImportPath checks if the module m's "module" path is actually
|
|
// an import path. If so, it adds the import path to the packages list
|
|
// and fixes the module path. Modifies m.
|
|
//
|
|
// Does nothing if the module path is already correct, or isn't recognized
|
|
// by the proxy at all.
|
|
func extractImportPath(m *Module, pc *proxy.Client) {
|
|
path := m.Module
|
|
modulePath, err := pc.FindModule(m.Module)
|
|
if err != nil || // path doesn't contain a module, needs human review
|
|
path == modulePath { // path is already a module, no action needed
|
|
return
|
|
}
|
|
m.Module = modulePath
|
|
m.Packages = append(m.Packages, &Package{Package: path})
|
|
}
|
|
|
|
func (m *Module) hasVersions() bool {
|
|
return len(m.Versions) != 0 || len(m.NonGoVersions) != 0 || len(m.UnsupportedVersions) != 0
|
|
}
|
|
|
|
type majorInfo struct {
|
|
base string
|
|
high int
|
|
all map[int]bool
|
|
}
|
|
|
|
func majorToInt(maj string) (int, bool) {
|
|
if maj == "" {
|
|
return 0, true
|
|
}
|
|
i, err := strconv.Atoi(strings.TrimPrefix(maj, "/v"))
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return i, true
|
|
}
|
|
|
|
func intToMajor(i int) string {
|
|
if i == 0 {
|
|
return v0v1
|
|
}
|
|
return fmt.Sprintf("v%d", i)
|
|
}
|
|
|
|
func (r *Report) addMissingMajors(pc *proxy.Client) {
|
|
// Map from module v1 path to set of all listed major versions.
|
|
majorMap := make(map[string]*majorInfo)
|
|
for _, m := range r.Modules {
|
|
base, pathMajor, ok := module.SplitPathVersion(m.Module)
|
|
if !ok { // couldn't parse module path, skip
|
|
continue
|
|
}
|
|
i, ok := majorToInt(pathMajor)
|
|
if !ok { // invalid major version, skip
|
|
continue
|
|
}
|
|
v1Mod := modulePath(base, v0v1)
|
|
if majorMap[v1Mod] == nil {
|
|
majorMap[v1Mod] = &majorInfo{
|
|
base: base,
|
|
all: make(map[int]bool),
|
|
}
|
|
}
|
|
if i > majorMap[v1Mod].high {
|
|
majorMap[v1Mod].high = i
|
|
}
|
|
majorMap[v1Mod].all[i] = true
|
|
}
|
|
|
|
for _, mi := range majorMap {
|
|
for i := 0; i < mi.high; i++ {
|
|
if mi.all[i] {
|
|
continue
|
|
}
|
|
mod := modulePath(mi.base, intToMajor(i))
|
|
if !pc.ModuleExists(mod) {
|
|
continue
|
|
}
|
|
r.Modules = append(r.Modules, &Module{
|
|
Module: mod,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Module) splitByMajor(pc *proxy.Client) (modules []*Module) {
|
|
if stdlib.IsCmdModule(m.Module) || stdlib.IsStdModule(m.Module) || // no major versions for stdlib
|
|
!m.hasVersions() || // no versions -> no need to split
|
|
strings.HasPrefix(m.Module, "gopkg.in/") { // for now, don't attempt to split gopkg.in modules
|
|
return []*Module{m}
|
|
}
|
|
|
|
base, _, ok := module.SplitPathVersion(m.Module)
|
|
if !ok { // couldn't parse module path, don't attempt to fix
|
|
return []*Module{m}
|
|
}
|
|
v1Mod := modulePath(base, v0v1)
|
|
rawMajorMap := m.byMajor()
|
|
validated := make(map[string]*allVersions)
|
|
|
|
for maj, av := range rawMajorMap {
|
|
mod := modulePath(base, maj)
|
|
// If the module at the major version doesn't exist, add the
|
|
// version to the v1 module.
|
|
if mod == v1Mod || !pc.ModuleExists(mod) {
|
|
if validated[v1Mod] == nil {
|
|
validated[v1Mod] = new(allVersions)
|
|
}
|
|
validated[v1Mod].add(av)
|
|
continue
|
|
}
|
|
validated[mod] = av
|
|
}
|
|
|
|
// Ensure that the original module mentioned is preserved,
|
|
// if it exists, even if there are now no versions associated
|
|
// with it.
|
|
original := m.Module
|
|
if _, ok := validated[original]; !ok {
|
|
if pc.ModuleExists(original) {
|
|
validated[original] = &allVersions{}
|
|
}
|
|
}
|
|
|
|
for mod, av := range validated {
|
|
mc := m.copy()
|
|
mc.Module = mod
|
|
mc.Versions = av.standard
|
|
mc.UnsupportedVersions = av.unsupported
|
|
mc.NonGoVersions = av.nonGo
|
|
if !inVulnerableRange(mc.Versions, mc.VulnerableAt) {
|
|
mc.VulnerableAt = nil // needs to be re-generated
|
|
}
|
|
if mod == v1Mod {
|
|
addIncompatible(mc, pc)
|
|
}
|
|
canonicalize(mc, pc)
|
|
modules = append(modules, mc)
|
|
}
|
|
|
|
return modules
|
|
}
|
|
|
|
func inVulnerableRange(vs Versions, v *Version) bool {
|
|
if v == nil {
|
|
return false
|
|
}
|
|
|
|
rs, err := vs.ToSemverRanges()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
affected, err := osvutils.AffectsSemver(rs, v.Version)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return affected
|
|
}
|
|
|
|
var transforms = map[string]string{
|
|
"github.com/mattermost/mattermost/server": "github.com/mattermost/mattermost-server",
|
|
"github.com/mattermost/mattermost/server/v5": "github.com/mattermost/mattermost-server/v5",
|
|
"github.com/mattermost/mattermost/server/v6": "github.com/mattermost/mattermost-server/v6",
|
|
}
|
|
|
|
func transform(m string) string {
|
|
if t, ok := transforms[m]; ok {
|
|
return t
|
|
}
|
|
return m
|
|
}
|
|
|
|
func modulePath(prefix, pathMajor string) string {
|
|
raw := func(prefix, pathMajor string) string {
|
|
if pathMajor == v0v1 {
|
|
return prefix
|
|
}
|
|
return prefix + "/" + pathMajor
|
|
}
|
|
return transform(raw(prefix, pathMajor))
|
|
}
|
|
|
|
func (m *Module) copy() *Module {
|
|
return &Module{
|
|
Module: m.Module,
|
|
Versions: m.Versions.copy(),
|
|
NonGoVersions: m.NonGoVersions.copy(),
|
|
UnsupportedVersions: m.UnsupportedVersions.copy(),
|
|
VulnerableAt: m.VulnerableAt.copy(),
|
|
VulnerableAtRequires: slices.Clone(m.VulnerableAtRequires),
|
|
Packages: copyPackages(m.Packages),
|
|
FixLinks: slices.Clone(m.FixLinks),
|
|
}
|
|
}
|
|
|
|
func (vs Versions) copy() Versions {
|
|
if vs == nil {
|
|
return nil
|
|
}
|
|
vsc := make(Versions, len(vs))
|
|
for i, v := range vs {
|
|
vsc[i] = v.copy()
|
|
}
|
|
return vsc
|
|
}
|
|
|
|
func (v *Version) copy() *Version {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return &Version{
|
|
Type: v.Type,
|
|
Version: v.Version,
|
|
}
|
|
}
|
|
|
|
func copyPackages(ps []*Package) []*Package {
|
|
if ps == nil {
|
|
return nil
|
|
}
|
|
psc := make([]*Package, len(ps))
|
|
for i, p := range ps {
|
|
psc[i] = p.copy()
|
|
}
|
|
return psc
|
|
}
|
|
|
|
func (p *Package) copy() *Package {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
return &Package{
|
|
Package: p.Package,
|
|
GOOS: slices.Clone(p.GOOS),
|
|
GOARCH: slices.Clone(p.GOARCH),
|
|
Symbols: slices.Clone(p.Symbols),
|
|
DerivedSymbols: slices.Clone(p.DerivedSymbols),
|
|
ExcludedSymbols: slices.Clone(p.ExcludedSymbols),
|
|
SkipFixSymbols: p.SkipFixSymbols,
|
|
}
|
|
}
|
|
|
|
const (
|
|
v0 = "v0"
|
|
v1 = "v1"
|
|
v0v1 = "v0 or v1"
|
|
)
|
|
|
|
func major(v string) string {
|
|
m := version.Major(v)
|
|
if m == v0 || m == v1 {
|
|
return v0v1
|
|
}
|
|
return m
|
|
}
|
|
|
|
type allVersions struct {
|
|
standard, unsupported, nonGo Versions
|
|
}
|
|
|
|
func (a *allVersions) add(b *allVersions) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
a.standard = append(a.standard, b.standard...)
|
|
a.unsupported = append(a.unsupported, b.unsupported...)
|
|
a.nonGo = append(a.nonGo, b.nonGo...)
|
|
}
|
|
|
|
func (m *Module) byMajor() map[string]*allVersions {
|
|
mp := make(map[string]*allVersions)
|
|
getMajor := func(v *Version) string {
|
|
maj := major(v.Version)
|
|
if mp[maj] == nil {
|
|
mp[maj] = new(allVersions)
|
|
}
|
|
return maj
|
|
}
|
|
for _, v := range m.Versions {
|
|
maj := getMajor(v)
|
|
mp[maj].standard = append(mp[maj].standard, v)
|
|
}
|
|
for _, v := range m.UnsupportedVersions {
|
|
maj := getMajor(v)
|
|
mp[maj].unsupported = append(mp[maj].unsupported, v)
|
|
}
|
|
for _, v := range m.NonGoVersions {
|
|
maj := getMajor(v)
|
|
mp[maj].nonGo = append(mp[maj].nonGo, v)
|
|
}
|
|
return mp
|
|
}
|
|
|
|
// canonicalize attempts to canonicalize the module path,
|
|
// and updates the module path and packages list if successful.
|
|
// Modifies m.
|
|
//
|
|
// Does nothing if the module path is already canonical, or isn't recognized
|
|
// by the proxy at all.
|
|
func canonicalize(m *Module, pc *proxy.Client) {
|
|
if len(m.Versions) == 0 {
|
|
return // no versions, don't attempt to fix
|
|
}
|
|
|
|
canonical, err := commonCanonical(m, pc)
|
|
if err != nil {
|
|
return // no consistent canonical version found, don't attempt to fix
|
|
}
|
|
|
|
original := m.Module
|
|
m.Module = canonical
|
|
|
|
// Fix any package paths.
|
|
for _, p := range m.Packages {
|
|
if strings.HasPrefix(p.Package, original) {
|
|
p.Package = canonical + strings.TrimPrefix(p.Package, original)
|
|
}
|
|
}
|
|
}
|
|
|
|
func commonCanonical(m *Module, pc *proxy.Client) (string, error) {
|
|
if len(m.Versions) == 0 {
|
|
return m.Module, nil
|
|
}
|
|
|
|
canonical, err := pc.CanonicalModulePath(m.Module, m.Versions[0].Version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, v := range m.Versions {
|
|
current, err := pc.CanonicalModulePath(m.Module, v.Version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if current != canonical {
|
|
return "", fmt.Errorf("inconsistent canonical module paths: %s and %s", canonical, current)
|
|
}
|
|
}
|
|
return canonical, nil
|
|
}
|
|
|
|
// addIncompatible adds "+incompatible" to all versions where module@version
|
|
// does not exist but module@version+incompatible does exist.
|
|
// TODO(https://go.dev/issue/61769): Consider making this work for
|
|
// non-canonical versions too (example: GHSA-w4xh-w33p-4v29).
|
|
func addIncompatible(m *Module, pc *proxy.Client) {
|
|
tryAdd := func(v string) string {
|
|
if v == "" {
|
|
return v
|
|
}
|
|
if major(v) == v0v1 {
|
|
return v // +incompatible does not apply for major versions < 2
|
|
}
|
|
if pc.ModuleExistsAtTaggedVersion(m.Module, v) {
|
|
return v // module@version is already OK
|
|
}
|
|
if vi := v + "+incompatible"; pc.ModuleExistsAtTaggedVersion(m.Module, vi) {
|
|
return vi
|
|
}
|
|
return v // module@version+incompatible doesn't exist
|
|
}
|
|
for i, v := range m.Versions {
|
|
m.Versions[i].Version = tryAdd(v.Version)
|
|
}
|
|
}
|
|
|
|
func sortModules(ms []*Module) {
|
|
sort.SliceStable(ms, func(i, j int) bool {
|
|
m1, m2 := ms[i], ms[j]
|
|
|
|
// Break ties by versions, assuming the version list is sorted.
|
|
// If needed, further break ties by packages.
|
|
if m1.Module == m2.Module {
|
|
byPackage := func(m1, m2 *Module) bool {
|
|
pkgs1, pkgs2 := m1.Packages, m2.Packages
|
|
if len(pkgs1) == 0 {
|
|
return true
|
|
} else if len(pkgs2) == 0 {
|
|
return false
|
|
}
|
|
return pkgs1[0].Package < pkgs2[0].Package
|
|
}
|
|
|
|
vr1, vr2 := m1.Versions, m2.Versions
|
|
if len(vr1) == 0 && len(vr2) == 0 {
|
|
return byPackage(m1, m2)
|
|
} else if len(vr1) == 0 {
|
|
return true
|
|
} else if len(vr2) == 0 {
|
|
return false
|
|
}
|
|
|
|
v1, v2 := vr1[0], vr2[0]
|
|
if v1.Version == v2.Version {
|
|
return byPackage(m1, m2)
|
|
}
|
|
|
|
return version.Before(v1.Version, v2.Version)
|
|
}
|
|
|
|
// Sort by module base name then major version.
|
|
base1, major1, ok1 := module.SplitPathVersion(m1.Module)
|
|
base2, major2, ok2 := module.SplitPathVersion(m2.Module)
|
|
if !ok1 || !ok2 {
|
|
return m1.Module < m2.Module
|
|
}
|
|
|
|
if base1 == base2 {
|
|
i1, ok1 := majorToInt(major1)
|
|
i2, ok2 := majorToInt(major2)
|
|
if ok1 && ok2 {
|
|
return i1 < i2
|
|
}
|
|
return major1 < major2
|
|
}
|
|
|
|
return base1 < base2
|
|
})
|
|
}
|
|
|
|
// merge merges all modules with the same module & package info
|
|
// (but possibly different versions) into one.
|
|
func merge(ms []*Module) ([]*Module, error) {
|
|
type compMod struct {
|
|
path string
|
|
packages string // sorted, comma separated list of package names
|
|
}
|
|
|
|
toCompMod := func(m *Module) compMod {
|
|
var packages []string
|
|
for _, p := range m.Packages {
|
|
packages = append(packages, p.Package)
|
|
}
|
|
return compMod{
|
|
path: m.Module,
|
|
packages: strings.Join(packages, ","),
|
|
}
|
|
}
|
|
|
|
// only run if m1 and m2 are same except versions
|
|
// deletes vulnerable_at if set
|
|
merge := func(m1, m2 *Module) (*Module, error) {
|
|
merged, err := m1.Versions.mergeStrict(m2.Versions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not merge versions of module %s: %w", m1.Module, err)
|
|
}
|
|
return &Module{
|
|
Module: m1.Module,
|
|
Versions: merged,
|
|
UnsupportedVersions: m1.UnsupportedVersions.merge(m2.UnsupportedVersions),
|
|
NonGoVersions: m1.NonGoVersions.merge(m2.NonGoVersions),
|
|
Packages: m1.Packages,
|
|
}, nil
|
|
}
|
|
|
|
modules := make(map[compMod]*Module)
|
|
for _, m := range ms {
|
|
c := toCompMod(m)
|
|
mod, ok := modules[c]
|
|
if !ok {
|
|
modules[c] = m
|
|
} else {
|
|
merged, err := merge(mod, m)
|
|
if err != nil {
|
|
// For now, bail out if any module can't be merged.
|
|
// This could be improved by continuing to try even if
|
|
// some merges fail.
|
|
return nil, err
|
|
}
|
|
modules[c] = merged
|
|
}
|
|
}
|
|
|
|
return maps.Values(modules), nil
|
|
}
|
|
|
|
func (v Versions) merge(v2 Versions) Versions {
|
|
merged := append(slices.Clone(v), v2...)
|
|
merged.fix()
|
|
return merged
|
|
}
|
|
|
|
func (v Versions) mergeStrict(v2 Versions) (merged Versions, _ error) {
|
|
merged = v.merge(v2)
|
|
ranges, err := merged.ToSemverRanges()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := osvutils.ValidateRanges(ranges); err != nil {
|
|
return nil, err
|
|
}
|
|
return merged, nil
|
|
}
|
|
|
|
// FixReferences deletes some unneeded references, and attempts to fix reference types.
|
|
// Modifies r.
|
|
//
|
|
// Deletes:
|
|
// - "package"-type references
|
|
// - Go advisory references (these are redundant for us)
|
|
// - all advisories except the "best" one (if applicable)
|
|
//
|
|
// Changes:
|
|
// - reference type to "advisory" for GHSA and CVE links.
|
|
// - reference type to "fix" for Github pull requests and commit links in one of
|
|
// the affected modules
|
|
// - reference type to "report" for Github issues in one of
|
|
// the affected modules
|
|
func (r *Report) FixReferences() {
|
|
for _, ref := range r.References {
|
|
ref.URL = fixURL(ref.URL)
|
|
}
|
|
r.References = slices.DeleteFunc(r.References, func(ref *Reference) bool {
|
|
return ref.Type == osv.ReferenceTypePackage ||
|
|
idstr.IsGoAdvisory(ref.URL)
|
|
})
|
|
|
|
re := newRE(r)
|
|
|
|
aliases := r.Aliases()
|
|
for _, ref := range r.References {
|
|
switch re.Type(ref.URL, aliases) {
|
|
case urlTypeAdvisory:
|
|
ref.Type = osv.ReferenceTypeAdvisory
|
|
case urlTypeIssue:
|
|
ref.Type = osv.ReferenceTypeReport
|
|
case urlTypeFix:
|
|
ref.Type = osv.ReferenceTypeFix
|
|
case urlTypeWeb:
|
|
ref.Type = osv.ReferenceTypeWeb
|
|
}
|
|
}
|
|
|
|
// If this is a reviewed report, attempt to find the "best" advisory and delete others.
|
|
if r.IsReviewed() {
|
|
if bestAdvisory := bestAdvisory(r.References, r.Aliases()); bestAdvisory != "" {
|
|
isNotBest := func(ref *Reference) bool {
|
|
return ref.Type == osv.ReferenceTypeAdvisory && ref.URL != bestAdvisory
|
|
}
|
|
r.References = slices.DeleteFunc(r.References, isNotBest)
|
|
}
|
|
}
|
|
|
|
if r.countAdvisories() == 0 && r.needsAdvisory() {
|
|
if r.hasExternalSource() {
|
|
r.addSourceAdvisory()
|
|
} else if as := r.Aliases(); len(as) > 0 {
|
|
r.addAdvisory(as[0])
|
|
}
|
|
}
|
|
|
|
slices.SortFunc(r.References, func(a *Reference, b *Reference) int {
|
|
if a.Type == b.Type {
|
|
return strings.Compare(a.URL, b.URL)
|
|
}
|
|
return strings.Compare(string(a.Type), string(b.Type))
|
|
})
|
|
|
|
if len(r.References) == 0 {
|
|
r.References = nil
|
|
}
|
|
}
|
|
|
|
func (r *Report) hasExternalSource() bool {
|
|
return r.SourceMeta != nil && idstr.IsIdentifier(r.SourceMeta.ID)
|
|
}
|
|
|
|
func (r *Report) addAdvisory(id string) {
|
|
if link := idstr.AdvisoryLink(id); link != "" {
|
|
r.References = append(r.References, &Reference{
|
|
Type: osv.ReferenceTypeAdvisory,
|
|
URL: link,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (r *Report) addSourceAdvisory() {
|
|
srcID := r.SourceMeta.ID
|
|
for _, ref := range r.References {
|
|
if idstr.IsAdvisoryFor(ref.URL, srcID) {
|
|
ref.Type = osv.ReferenceTypeAdvisory
|
|
return
|
|
}
|
|
}
|
|
r.addAdvisory(srcID)
|
|
}
|
|
|
|
// bestAdvisory returns the URL of the "best" advisory in the references,
|
|
// or ("", false) if none can be found.
|
|
// Repository-level GHSAs are considered the best, followed by regular
|
|
// GHSAs, followed by CVEs.
|
|
// For now, if there are advisories mentioning two or more
|
|
// aliases of the same type, we don't try to determine which is best.
|
|
// (For example, if there are two advisories, referencing GHSA-1 and GHSA-2, we leave it
|
|
// to the triager to pick the best one.)
|
|
func bestAdvisory(refs []*Reference, aliases []string) string {
|
|
bestAdvisory := ""
|
|
bestType := advisoryTypeUnknown
|
|
ghsas, cves := make(map[string]bool), make(map[string]bool)
|
|
for _, ref := range refs {
|
|
if ref.Type != osv.ReferenceTypeAdvisory {
|
|
continue
|
|
}
|
|
alias, ok := idstr.IsAdvisoryForOneOf(ref.URL, aliases)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if t := advisoryTypeOf(ref.URL); t > bestType {
|
|
bestAdvisory = ref.URL
|
|
bestType = t
|
|
}
|
|
|
|
if idstr.IsGHSA(alias) {
|
|
ghsas[alias] = true
|
|
} else if idstr.IsCVE(alias) {
|
|
cves[alias] = true
|
|
}
|
|
}
|
|
|
|
if len(ghsas) > 1 || len(cves) > 1 {
|
|
return ""
|
|
}
|
|
|
|
return bestAdvisory
|
|
}
|
|
|
|
type urlType int
|
|
|
|
const (
|
|
urlTypeUnknown urlType = iota
|
|
urlTypeIssue
|
|
urlTypeFix
|
|
urlTypeAdvisory
|
|
urlTypeWeb
|
|
)
|
|
|
|
func (re *reportRE) Type(url string, aliases []string) urlType {
|
|
if _, ok := idstr.IsAdvisoryForOneOf(url, aliases); ok {
|
|
return urlTypeAdvisory
|
|
} else if idstr.IsAdvisory(url) {
|
|
// URLs that point to other vulns should not be considered
|
|
// advisories for this vuln.
|
|
return urlTypeWeb
|
|
}
|
|
|
|
switch {
|
|
case re.issue.MatchString(url):
|
|
return urlTypeIssue
|
|
case re.fix.MatchString(url):
|
|
return urlTypeFix
|
|
}
|
|
|
|
return urlTypeUnknown
|
|
}
|
|
|
|
type advisoryType int
|
|
|
|
// Advisory link types in ascending order of (likely) quality.
|
|
// In general, repo-level GHSAs tend to be the best because
|
|
// they are more likely to be directly created by a maintainer.
|
|
const (
|
|
advisoryTypeUnknown advisoryType = iota
|
|
advisoryTypeCVE
|
|
advisoryTypeGHSA
|
|
advisoryTypeGHSARepo
|
|
)
|
|
|
|
func advisoryTypeOf(url string) advisoryType {
|
|
switch {
|
|
case idstr.IsCVELink(url):
|
|
return advisoryTypeCVE
|
|
case idstr.IsGHSAGlobalLink(url):
|
|
return advisoryTypeGHSA
|
|
case idstr.IsGHSARepoLink(url):
|
|
return advisoryTypeGHSARepo
|
|
}
|
|
return advisoryTypeUnknown
|
|
}
|
|
|
|
type reportRE struct {
|
|
issue, fix *regexp.Regexp
|
|
}
|
|
|
|
func newRE(r *Report) *reportRE {
|
|
oneOfRE := func(s []string) string {
|
|
return `(` + strings.Join(s, "|") + `)`
|
|
}
|
|
|
|
// For now, this will not attempt to fix reference types for
|
|
// modules whose canonical names are different from their github path.
|
|
var modulePaths []string
|
|
for _, m := range r.Modules {
|
|
modulePaths = append(modulePaths, m.Module)
|
|
}
|
|
moduleRE := oneOfRE(modulePaths)
|
|
|
|
return &reportRE{
|
|
issue: regexp.MustCompile(`^https://` + moduleRE + `/issue(s?)/.*$`),
|
|
fix: regexp.MustCompile(`^https://` + moduleRE + `/(commit(s?)|pull)/.*$`),
|
|
}
|
|
}
|