Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 1 | // Copyright 2023 The Go Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | package report |
| 6 | |
| 7 | import ( |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 8 | "errors" |
| 9 | "fmt" |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 10 | "regexp" |
Tatiana Bradley | 4c0120a | 2023-06-15 15:09:12 -0400 | [diff] [blame] | 11 | "sort" |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 12 | "strconv" |
Tatiana Bradley | 23bd6a8 | 2023-06-21 15:43:00 -0400 | [diff] [blame] | 13 | "strings" |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 14 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 15 | "golang.org/x/exp/maps" |
Tatiana Bradley | 3bd4bef | 2023-09-18 17:09:17 -0400 | [diff] [blame] | 16 | "golang.org/x/exp/slices" |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 17 | "golang.org/x/mod/module" |
Tatiana Bradley | 685ac19 | 2024-04-22 13:35:43 -0400 | [diff] [blame] | 18 | "golang.org/x/vulndb/internal/idstr" |
Tatiana Bradley | 3754b2a | 2024-02-07 12:23:17 -0500 | [diff] [blame] | 19 | "golang.org/x/vulndb/internal/osv" |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 20 | "golang.org/x/vulndb/internal/osvutils" |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 21 | "golang.org/x/vulndb/internal/proxy" |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 22 | "golang.org/x/vulndb/internal/stdlib" |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 23 | "golang.org/x/vulndb/internal/version" |
| 24 | ) |
| 25 | |
Tatiana Bradley | 24e908f | 2023-08-29 14:10:18 -0400 | [diff] [blame] | 26 | func (r *Report) Fix(pc *proxy.Client) { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 27 | r.deleteNotes(NoteTypeFix) |
Tim King | 878e33a | 2024-03-21 13:50:11 -0700 | [diff] [blame] | 28 | expandGitCommits(r) |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 29 | _ = r.FixModules(pc) |
Tatiana Bradley | 0c44de6 | 2024-02-06 13:57:48 -0500 | [diff] [blame] | 30 | r.FixText() |
Tatiana Bradley | 3754b2a | 2024-02-07 12:23:17 -0500 | [diff] [blame] | 31 | r.FixReferences() |
Tatiana Bradley | 0c44de6 | 2024-02-06 13:57:48 -0500 | [diff] [blame] | 32 | } |
| 33 | |
| 34 | func (r *Report) FixText() { |
Tatiana Bradley | 23bd6a8 | 2023-06-21 15:43:00 -0400 | [diff] [blame] | 35 | fixLines := func(sp *string) { |
| 36 | *sp = fixLineLength(*sp, maxLineLength) |
| 37 | } |
Tatiana Bradley | 79523f1 | 2023-11-14 11:10:14 -0500 | [diff] [blame] | 38 | fixLines((*string)(&r.Summary)) |
Tatiana Bradley | 11844d7 | 2023-11-14 11:19:03 -0500 | [diff] [blame] | 39 | fixLines((*string)(&r.Description)) |
Tatiana Bradley | 23bd6a8 | 2023-06-21 15:43:00 -0400 | [diff] [blame] | 40 | if r.CVEMetadata != nil { |
| 41 | fixLines(&r.CVEMetadata.Description) |
| 42 | } |
Tatiana Bradley | bbf0d71 | 2024-04-05 12:52:03 -0400 | [diff] [blame] | 43 | |
| 44 | r.fixSummary() |
| 45 | } |
| 46 | |
| 47 | func (r *Report) fixSummary() { |
| 48 | summary := r.Summary.String() |
| 49 | |
| 50 | // If there is no summary, create a basic one. |
| 51 | if summary == "" { |
| 52 | if aliases := r.Aliases(); len(aliases) != 0 { |
| 53 | summary = aliases[0] |
| 54 | } else { |
| 55 | summary = "Vulnerability" |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // Add a path if one exists and is needed. |
| 60 | if paths := r.nonStdPaths(); len(paths) > 0 && !containsPath(summary, paths) { |
Tatiana Bradley | e5d28b9 | 2024-07-23 16:58:12 -0400 | [diff] [blame] | 61 | summary = fmt.Sprintf("%s in %s", summary, stripMajor(paths[0])) |
Tatiana Bradley | bbf0d71 | 2024-04-05 12:52:03 -0400 | [diff] [blame] | 62 | } |
| 63 | |
Tatiana Bradley | 85d976c | 2024-06-26 16:28:58 -0400 | [diff] [blame] | 64 | r.Summary = Summary(fixSpelling(summary)) |
| 65 | } |
| 66 | |
Tatiana Bradley | e5d28b9 | 2024-07-23 16:58:12 -0400 | [diff] [blame] | 67 | func stripMajor(path string) string { |
| 68 | base, _, ok := module.SplitPathVersion(path) |
| 69 | if !ok { |
| 70 | return path |
| 71 | } |
| 72 | return base |
| 73 | } |
| 74 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 75 | func (v *Version) commitHashToVersion(modulePath string, pc *proxy.Client) { |
| 76 | if v == nil { |
| 77 | return |
| 78 | } |
| 79 | |
| 80 | vv := v.Version |
| 81 | if version.IsCommitHash(vv) { |
| 82 | if c, err := pc.CanonicalModuleVersion(modulePath, vv); err == nil { // no error |
| 83 | v.Version = c |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
Tatiana Bradley | c779530 | 2023-08-02 15:17:35 -0400 | [diff] [blame] | 88 | // FixVersions replaces each version with its canonical form (if possible), |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 89 | // sorts version ranges, and moves versions to their proper spot. |
Tatiana Bradley | 24e908f | 2023-08-29 14:10:18 -0400 | [diff] [blame] | 90 | func (m *Module) FixVersions(pc *proxy.Client) { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 91 | for _, v := range m.Versions { |
| 92 | v.commitHashToVersion(m.Module, pc) |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 93 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 94 | m.VulnerableAt.commitHashToVersion(m.Module, pc) |
Tatiana Bradley | 4c0120a | 2023-06-15 15:09:12 -0400 | [diff] [blame] | 95 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 96 | m.Versions.fix() |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 97 | m.UnsupportedVersions.fix() |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 98 | m.VulnerableAt.fix() |
Tatiana Bradley | 4c0120a | 2023-06-15 15:09:12 -0400 | [diff] [blame] | 99 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 100 | if pc != nil && !m.IsFirstParty() { |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 101 | found, notFound, _ := m.classifyVersions(pc) |
| 102 | if len(notFound) != 0 { |
| 103 | m.Versions = found |
| 104 | m.NonGoVersions = append(m.NonGoVersions, notFound...) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 105 | } |
| 106 | } |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 107 | } |
| 108 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 109 | func (v *Version) fix() { |
| 110 | if v == nil { |
| 111 | return |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 112 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 113 | vv := version.TrimPrefix(v.Version) |
| 114 | if version.IsValid(vv) { |
| 115 | vv = version.Canonical(vv) |
| 116 | } |
| 117 | v.Version = vv |
Tatiana Bradley | 95f52fc | 2023-08-28 16:02:46 -0400 | [diff] [blame] | 118 | } |
| 119 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 120 | func (vs *Versions) fix() { |
| 121 | for i := range *vs { |
| 122 | (*vs)[i].fix() |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 123 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 124 | sort.SliceStable(*vs, func(i, j int) bool { |
| 125 | return version.Before((*vs)[i].Version, (*vs)[j].Version) |
| 126 | }) |
| 127 | // Remove duplicates. |
| 128 | *vs = slices.Compact(*vs) |
| 129 | *vs = slices.CompactFunc(*vs, func(a, b *Version) bool { |
| 130 | return a.Type == b.Type && a.Version == b.Version |
| 131 | }) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 132 | } |
| 133 | |
| 134 | func (m *Module) fixVulnerableAt(pc *proxy.Client) error { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 135 | if m.VulnerableAt != nil { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 136 | return nil |
| 137 | } |
| 138 | if m.IsFirstParty() { |
| 139 | return fmt.Errorf("not implemented for std/cmd") |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 140 | } |
Tatiana Bradley | 95f52fc | 2023-08-28 16:02:46 -0400 | [diff] [blame] | 141 | // Don't attempt to guess if the given version ranges don't make sense. |
Tatiana Bradley | 477a075 | 2023-09-21 14:11:05 -0400 | [diff] [blame] | 142 | if err := m.checkModVersions(pc); err != nil { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 143 | return err |
Tatiana Bradley | 95f52fc | 2023-08-28 16:02:46 -0400 | [diff] [blame] | 144 | } |
Tatiana Bradley | 24e908f | 2023-08-29 14:10:18 -0400 | [diff] [blame] | 145 | v, err := m.guessVulnerableAt(pc) |
Tatiana Bradley | 95f52fc | 2023-08-28 16:02:46 -0400 | [diff] [blame] | 146 | if err != nil { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 147 | return err |
Tatiana Bradley | 95f52fc | 2023-08-28 16:02:46 -0400 | [diff] [blame] | 148 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 149 | m.VulnerableAt = VulnerableAt(v) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 150 | return nil |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 151 | } |
| 152 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 153 | var errZeroPseudo = errors.New("cannot auto-guess when fixed version is 0.0.0 pseudo-version") |
| 154 | |
| 155 | // Find the latest fixed and introduced version, assuming the version |
| 156 | // ranges are sorted and valid. |
| 157 | func (vs Versions) latestVersions() (introduced, fixed *Version) { |
| 158 | if len(vs) == 0 { |
| 159 | return |
| 160 | } |
| 161 | last := vs[len(vs)-1] |
| 162 | if last.IsIntroduced() { |
| 163 | introduced = last |
| 164 | return |
| 165 | } |
| 166 | fixed = last |
| 167 | if len(vs) > 1 { |
| 168 | if penultimate := vs[len(vs)-2]; penultimate.IsIntroduced() { |
| 169 | introduced = penultimate |
| 170 | } |
| 171 | } |
| 172 | return |
| 173 | } |
| 174 | |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 175 | // guessVulnerableAt attempts to find a vulnerable_at |
Tatiana Bradley | 95f52fc | 2023-08-28 16:02:46 -0400 | [diff] [blame] | 176 | // version using the module proxy, assuming that the version ranges |
| 177 | // have already been validated. |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 178 | // If there is no fix, the latest version is used. |
Tatiana Bradley | 8ab4518 | 2023-08-30 15:04:06 -0400 | [diff] [blame] | 179 | func (m *Module) guessVulnerableAt(pc *proxy.Client) (v string, err error) { |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 180 | if m.IsFirstParty() { |
| 181 | return "", errors.New("cannot auto-guess vulnerable_at for first-party modules") |
| 182 | } |
| 183 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 184 | introduced, fixed := m.Versions.latestVersions() |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 185 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 186 | // If there is no latest fix, find the latest version of the module. |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 187 | if fixed == nil { |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 188 | latest, err := pc.Latest(m.Module) |
| 189 | if err != nil || latest == "" { |
Tatiana Bradley | 207b5b9 | 2023-08-23 15:12:16 -0400 | [diff] [blame] | 190 | return "", fmt.Errorf("no fix, but could not find latest version from proxy: %s", err) |
| 191 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 192 | if introduced != nil && version.Before(latest, introduced.Version) { |
| 193 | return "", fmt.Errorf("latest version (%s) is before last introduced version", latest) |
| 194 | } |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 195 | return latest, nil |
| 196 | } |
| 197 | |
| 198 | // If the latest fixed version is a 0.0.0 pseudo-version, or not a valid version, |
| 199 | // don't attempt to determine the vulnerable_at version. |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 200 | if !version.IsValid(fixed.Version) { |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 201 | return "", errors.New("cannot auto-guess when fixed version is invalid") |
| 202 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 203 | if strings.HasPrefix(fixed.Version, "0.0.0-") { |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 204 | return "", errZeroPseudo |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 205 | } |
| 206 | |
| 207 | // Otherwise, find the version right before the fixed version. |
| 208 | vs, err := pc.Versions(m.Module) |
| 209 | if err != nil { |
| 210 | return "", fmt.Errorf("could not find versions from proxy: %s", err) |
| 211 | } |
| 212 | for i := len(vs) - 1; i >= 0; i-- { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 213 | if version.Before(vs[i], fixed.Version) { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 214 | // Make sure the version is >= the latest introduced version. |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 215 | if introduced == nil || !version.Before(vs[i], introduced.Version) { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 216 | return vs[i], nil |
| 217 | } |
Tatiana Bradley | 1468b95 | 2023-06-22 17:25:14 -0400 | [diff] [blame] | 218 | } |
| 219 | } |
| 220 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 221 | return "", errors.New("could not find tagged version between introduced and fixed") |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 222 | } |
| 223 | |
Tatiana Bradley | 23bd6a8 | 2023-06-21 15:43:00 -0400 | [diff] [blame] | 224 | // fixLineLength returns a copy of s with all lines trimmed to <=n characters |
| 225 | // (with the exception of single-word lines). |
| 226 | // It preserves paragraph breaks (indicated by "\n\n") and markdown-style list |
| 227 | // breaks. |
| 228 | func fixLineLength(s string, n int) string { |
| 229 | var result strings.Builder |
| 230 | result.Grow(len(s)) |
| 231 | for i, paragraph := range strings.Split(toParagraphs(s), "\n\n") { |
| 232 | if i > 0 { |
| 233 | result.WriteString("\n\n") |
| 234 | } |
| 235 | var lines []string |
| 236 | for _, forcedLine := range strings.Split(paragraph, "\n") { |
| 237 | words := strings.Split(forcedLine, " ") |
| 238 | start, length := 0, 0 |
| 239 | for k, word := range words { |
| 240 | newLength := length + len(word) |
| 241 | if length > 0 { |
| 242 | newLength++ // space character |
| 243 | } |
| 244 | if newLength <= n { |
| 245 | length = newLength |
| 246 | continue |
| 247 | } |
| 248 | // Adding the word would put the line over the max length, |
| 249 | // so add the line as is (if it is non-empty). |
| 250 | if length > 0 { |
| 251 | lines = append(lines, strings.Join(words[start:k], " ")) |
| 252 | } |
| 253 | // Begin a new line with just the word. |
| 254 | start, length = k, len(word) |
| 255 | } |
| 256 | // Add the last line. |
| 257 | if length > 0 { |
| 258 | lines = append(lines, strings.Join(words[start:], " ")) |
| 259 | } |
| 260 | } |
| 261 | result.WriteString(strings.Join(lines, "\n")) |
| 262 | } |
| 263 | return result.String() |
| 264 | } |
| 265 | |
Tatiana Bradley | 3816721 | 2023-05-22 13:59:47 -0400 | [diff] [blame] | 266 | var urlReplacements = []struct { |
| 267 | re *regexp.Regexp |
| 268 | repl string |
| 269 | }{{ |
| 270 | regexp.MustCompile(`golang.org`), |
| 271 | `go.dev`, |
| 272 | }, { |
| 273 | regexp.MustCompile(`https?://groups.google.com/forum/\#\![^/]*/([^/]+)/([^/]+)/(.*)`), |
| 274 | |
| 275 | `https://groups.google.com/g/$1/c/$2/m/$3`, |
| 276 | }, { |
| 277 | regexp.MustCompile(`.*github.com/golang/go/issues`), |
| 278 | `https://go.dev/issue`, |
| 279 | }, { |
| 280 | regexp.MustCompile(`.*github.com/golang/go/commit`), |
| 281 | `https://go.googlesource.com/+`, |
| 282 | }, |
| 283 | } |
| 284 | |
| 285 | func fixURL(u string) string { |
| 286 | for _, repl := range urlReplacements { |
| 287 | u = repl.re.ReplaceAllString(u, repl.repl) |
| 288 | } |
| 289 | return u |
| 290 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 291 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 292 | func (r *Report) FixModules(pc *proxy.Client) (errs error) { |
| 293 | var fixed []*Module |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 294 | for _, m := range r.Modules { |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 295 | m.Module = transform(m.Module) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 296 | extractImportPath(m, pc) |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 297 | fixed = append(fixed, m.splitByMajor(pc)...) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 298 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 299 | r.Modules = fixed |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 300 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 301 | merged, err := merge(fixed) |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 302 | if err != nil { |
| 303 | r.AddNote(NoteTypeFix, "module merge error: %s", err) |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 304 | errs = errors.Join(errs, err) |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 305 | } else { |
| 306 | r.Modules = merged |
| 307 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 308 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 309 | // For unreviewed reports, assume that all major versions |
| 310 | // up to the highest mentioned are affected at all versions. |
Tatiana Bradley | bca6ae2 | 2024-07-15 16:36:44 -0400 | [diff] [blame] | 311 | if r.IsUnreviewed() { |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 312 | r.addMissingMajors(pc) |
| 313 | } |
| 314 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 315 | // Fix the versions *after* the modules have been merged. |
| 316 | for _, m := range r.Modules { |
| 317 | m.FixVersions(pc) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 318 | if err := m.fixVulnerableAt(pc); err != nil { |
| 319 | r.AddNote(NoteTypeFix, "%s: could not add vulnerable_at: %v", m.Module, err) |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 320 | errs = errors.Join(errs, err) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 321 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 322 | } |
| 323 | |
| 324 | sortModules(r.Modules) |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 325 | return errs |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 326 | } |
| 327 | |
| 328 | // extractImportPath checks if the module m's "module" path is actually |
| 329 | // an import path. If so, it adds the import path to the packages list |
| 330 | // and fixes the module path. Modifies m. |
| 331 | // |
| 332 | // Does nothing if the module path is already correct, or isn't recognized |
| 333 | // by the proxy at all. |
| 334 | func extractImportPath(m *Module, pc *proxy.Client) { |
| 335 | path := m.Module |
| 336 | modulePath, err := pc.FindModule(m.Module) |
| 337 | if err != nil || // path doesn't contain a module, needs human review |
| 338 | path == modulePath { // path is already a module, no action needed |
| 339 | return |
| 340 | } |
| 341 | m.Module = modulePath |
| 342 | m.Packages = append(m.Packages, &Package{Package: path}) |
| 343 | } |
| 344 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 345 | func (m *Module) hasVersions() bool { |
| 346 | return len(m.Versions) != 0 || len(m.NonGoVersions) != 0 || len(m.UnsupportedVersions) != 0 |
| 347 | } |
| 348 | |
| 349 | type majorInfo struct { |
| 350 | base string |
| 351 | high int |
| 352 | all map[int]bool |
| 353 | } |
| 354 | |
| 355 | func majorToInt(maj string) (int, bool) { |
| 356 | if maj == "" { |
| 357 | return 0, true |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 358 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 359 | i, err := strconv.Atoi(strings.TrimPrefix(maj, "/v")) |
| 360 | if err != nil { |
| 361 | return 0, false |
| 362 | } |
| 363 | return i, true |
| 364 | } |
| 365 | |
| 366 | func intToMajor(i int) string { |
| 367 | if i == 0 { |
| 368 | return v0v1 |
| 369 | } |
| 370 | return fmt.Sprintf("v%d", i) |
| 371 | } |
| 372 | |
| 373 | func (r *Report) addMissingMajors(pc *proxy.Client) { |
| 374 | // Map from module v1 path to set of all listed major versions. |
| 375 | majorMap := make(map[string]*majorInfo) |
| 376 | for _, m := range r.Modules { |
| 377 | base, pathMajor, ok := module.SplitPathVersion(m.Module) |
| 378 | if !ok { // couldn't parse module path, skip |
| 379 | continue |
| 380 | } |
| 381 | i, ok := majorToInt(pathMajor) |
| 382 | if !ok { // invalid major version, skip |
| 383 | continue |
| 384 | } |
| 385 | v1Mod := modulePath(base, v0v1) |
| 386 | if majorMap[v1Mod] == nil { |
| 387 | majorMap[v1Mod] = &majorInfo{ |
| 388 | base: base, |
| 389 | all: make(map[int]bool), |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 390 | } |
| 391 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 392 | if i > majorMap[v1Mod].high { |
| 393 | majorMap[v1Mod].high = i |
| 394 | } |
| 395 | majorMap[v1Mod].all[i] = true |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 396 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 397 | |
| 398 | for _, mi := range majorMap { |
| 399 | for i := 0; i < mi.high; i++ { |
| 400 | if mi.all[i] { |
| 401 | continue |
| 402 | } |
| 403 | mod := modulePath(mi.base, intToMajor(i)) |
| 404 | if !pc.ModuleExists(mod) { |
| 405 | continue |
| 406 | } |
| 407 | r.Modules = append(r.Modules, &Module{ |
| 408 | Module: mod, |
| 409 | }) |
| 410 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 411 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 412 | } |
| 413 | |
| 414 | func (m *Module) splitByMajor(pc *proxy.Client) (modules []*Module) { |
| 415 | if stdlib.IsCmdModule(m.Module) || stdlib.IsStdModule(m.Module) || // no major versions for stdlib |
| 416 | !m.hasVersions() || // no versions -> no need to split |
| 417 | strings.HasPrefix(m.Module, "gopkg.in/") { // for now, don't attempt to split gopkg.in modules |
| 418 | return []*Module{m} |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 419 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 420 | |
| 421 | base, _, ok := module.SplitPathVersion(m.Module) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 422 | if !ok { // couldn't parse module path, don't attempt to fix |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 423 | return []*Module{m} |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 424 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 425 | v1Mod := modulePath(base, v0v1) |
| 426 | rawMajorMap := m.byMajor() |
| 427 | validated := make(map[string]*allVersions) |
| 428 | |
| 429 | for maj, av := range rawMajorMap { |
| 430 | mod := modulePath(base, maj) |
| 431 | // If the module at the major version doesn't exist, add the |
| 432 | // version to the v1 module. |
| 433 | if mod == v1Mod || !pc.ModuleExists(mod) { |
| 434 | if validated[v1Mod] == nil { |
| 435 | validated[v1Mod] = new(allVersions) |
| 436 | } |
| 437 | validated[v1Mod].add(av) |
| 438 | continue |
| 439 | } |
| 440 | validated[mod] = av |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 441 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 442 | |
| 443 | // Ensure that the original module mentioned is preserved, |
| 444 | // if it exists, even if there are now no versions associated |
| 445 | // with it. |
| 446 | original := m.Module |
| 447 | if _, ok := validated[original]; !ok { |
| 448 | if pc.ModuleExists(original) { |
| 449 | validated[original] = &allVersions{} |
| 450 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 451 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 452 | |
| 453 | for mod, av := range validated { |
| 454 | mc := m.copy() |
| 455 | mc.Module = mod |
| 456 | mc.Versions = av.standard |
| 457 | mc.UnsupportedVersions = av.unsupported |
| 458 | mc.NonGoVersions = av.nonGo |
| 459 | mc.VulnerableAt = nil // needs to be re-generated |
| 460 | if mod == v1Mod { |
| 461 | addIncompatible(mc, pc) |
| 462 | } |
| 463 | canonicalize(mc, pc) |
| 464 | modules = append(modules, mc) |
| 465 | } |
| 466 | |
| 467 | return modules |
| 468 | } |
| 469 | |
| 470 | var transforms = map[string]string{ |
| 471 | "github.com/mattermost/mattermost/server": "github.com/mattermost/mattermost-server", |
| 472 | "github.com/mattermost/mattermost/server/v5": "github.com/mattermost/mattermost-server/v5", |
| 473 | "github.com/mattermost/mattermost/server/v6": "github.com/mattermost/mattermost-server/v6", |
| 474 | } |
| 475 | |
| 476 | func transform(m string) string { |
| 477 | if t, ok := transforms[m]; ok { |
| 478 | return t |
| 479 | } |
| 480 | return m |
| 481 | } |
| 482 | |
| 483 | func modulePath(prefix, pathMajor string) string { |
| 484 | raw := func(prefix, pathMajor string) string { |
| 485 | if pathMajor == v0v1 { |
| 486 | return prefix |
| 487 | } |
| 488 | return prefix + "/" + pathMajor |
| 489 | } |
| 490 | return transform(raw(prefix, pathMajor)) |
| 491 | } |
| 492 | |
| 493 | func (m *Module) copy() *Module { |
| 494 | return &Module{ |
| 495 | Module: m.Module, |
| 496 | Versions: m.Versions.copy(), |
| 497 | NonGoVersions: m.NonGoVersions.copy(), |
| 498 | UnsupportedVersions: m.UnsupportedVersions.copy(), |
| 499 | VulnerableAt: m.VulnerableAt.copy(), |
| 500 | VulnerableAtRequires: slices.Clone(m.VulnerableAtRequires), |
| 501 | Packages: copyPackages(m.Packages), |
| 502 | FixLinks: slices.Clone(m.FixLinks), |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | func (vs Versions) copy() Versions { |
| 507 | if vs == nil { |
| 508 | return nil |
| 509 | } |
| 510 | vsc := make(Versions, len(vs)) |
| 511 | for i, v := range vs { |
| 512 | vsc[i] = v.copy() |
| 513 | } |
| 514 | return vsc |
| 515 | } |
| 516 | |
| 517 | func (v *Version) copy() *Version { |
| 518 | if v == nil { |
| 519 | return nil |
| 520 | } |
| 521 | return &Version{ |
| 522 | Type: v.Type, |
| 523 | Version: v.Version, |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | func copyPackages(ps []*Package) []*Package { |
| 528 | if ps == nil { |
| 529 | return nil |
| 530 | } |
| 531 | psc := make([]*Package, len(ps)) |
| 532 | for i, p := range ps { |
| 533 | psc[i] = p.copy() |
| 534 | } |
| 535 | return psc |
| 536 | } |
| 537 | |
| 538 | func (p *Package) copy() *Package { |
| 539 | if p == nil { |
| 540 | return nil |
| 541 | } |
| 542 | return &Package{ |
| 543 | Package: p.Package, |
| 544 | GOOS: slices.Clone(p.GOOS), |
| 545 | GOARCH: slices.Clone(p.GOARCH), |
| 546 | Symbols: slices.Clone(p.Symbols), |
| 547 | DerivedSymbols: slices.Clone(p.DerivedSymbols), |
| 548 | ExcludedSymbols: slices.Clone(p.ExcludedSymbols), |
Tatiana Bradley | ebcb244 | 2024-07-17 17:17:22 -0400 | [diff] [blame] | 549 | SkipFixSymbols: p.SkipFixSymbols, |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 550 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 551 | } |
| 552 | |
| 553 | const ( |
| 554 | v0 = "v0" |
| 555 | v1 = "v1" |
| 556 | v0v1 = "v0 or v1" |
| 557 | ) |
| 558 | |
| 559 | func major(v string) string { |
| 560 | m := version.Major(v) |
| 561 | if m == v0 || m == v1 { |
| 562 | return v0v1 |
| 563 | } |
| 564 | return m |
| 565 | } |
| 566 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 567 | type allVersions struct { |
| 568 | standard, unsupported, nonGo Versions |
| 569 | } |
| 570 | |
| 571 | func (a *allVersions) add(b *allVersions) { |
| 572 | if b == nil { |
| 573 | return |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 574 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 575 | a.standard = append(a.standard, b.standard...) |
| 576 | a.unsupported = append(a.unsupported, b.unsupported...) |
| 577 | a.nonGo = append(a.nonGo, b.nonGo...) |
| 578 | } |
| 579 | |
| 580 | func (m *Module) byMajor() map[string]*allVersions { |
| 581 | mp := make(map[string]*allVersions) |
| 582 | getMajor := func(v *Version) string { |
| 583 | maj := major(v.Version) |
| 584 | if mp[maj] == nil { |
| 585 | mp[maj] = new(allVersions) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 586 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 587 | return maj |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 588 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 589 | for _, v := range m.Versions { |
| 590 | maj := getMajor(v) |
| 591 | mp[maj].standard = append(mp[maj].standard, v) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 592 | } |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 593 | for _, v := range m.UnsupportedVersions { |
| 594 | maj := getMajor(v) |
| 595 | mp[maj].unsupported = append(mp[maj].unsupported, v) |
| 596 | } |
| 597 | for _, v := range m.NonGoVersions { |
| 598 | maj := getMajor(v) |
| 599 | mp[maj].nonGo = append(mp[maj].nonGo, v) |
| 600 | } |
| 601 | return mp |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 602 | } |
| 603 | |
| 604 | // canonicalize attempts to canonicalize the module path, |
| 605 | // and updates the module path and packages list if successful. |
| 606 | // Modifies m. |
| 607 | // |
| 608 | // Does nothing if the module path is already canonical, or isn't recognized |
| 609 | // by the proxy at all. |
| 610 | func canonicalize(m *Module, pc *proxy.Client) { |
| 611 | if len(m.Versions) == 0 { |
| 612 | return // no versions, don't attempt to fix |
| 613 | } |
| 614 | |
| 615 | canonical, err := commonCanonical(m, pc) |
| 616 | if err != nil { |
| 617 | return // no consistent canonical version found, don't attempt to fix |
| 618 | } |
| 619 | |
| 620 | original := m.Module |
| 621 | m.Module = canonical |
| 622 | |
| 623 | // Fix any package paths. |
| 624 | for _, p := range m.Packages { |
| 625 | if strings.HasPrefix(p.Package, original) { |
| 626 | p.Package = canonical + strings.TrimPrefix(p.Package, original) |
| 627 | } |
| 628 | } |
| 629 | } |
| 630 | |
| 631 | func commonCanonical(m *Module, pc *proxy.Client) (string, error) { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 632 | if len(m.Versions) == 0 { |
| 633 | return m.Module, nil |
| 634 | } |
| 635 | |
| 636 | canonical, err := pc.CanonicalModulePath(m.Module, m.Versions[0].Version) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 637 | if err != nil { |
| 638 | return "", err |
| 639 | } |
| 640 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 641 | for _, v := range m.Versions { |
| 642 | current, err := pc.CanonicalModulePath(m.Module, v.Version) |
| 643 | if err != nil { |
| 644 | return "", err |
| 645 | } |
| 646 | if current != canonical { |
| 647 | return "", fmt.Errorf("inconsistent canonical module paths: %s and %s", canonical, current) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 648 | } |
| 649 | } |
| 650 | return canonical, nil |
| 651 | } |
| 652 | |
| 653 | // addIncompatible adds "+incompatible" to all versions where module@version |
| 654 | // does not exist but module@version+incompatible does exist. |
| 655 | // TODO(https://go.dev/issue/61769): Consider making this work for |
| 656 | // non-canonical versions too (example: GHSA-w4xh-w33p-4v29). |
| 657 | func addIncompatible(m *Module, pc *proxy.Client) { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 658 | tryAdd := func(v string) string { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 659 | if v == "" { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 660 | return v |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 661 | } |
| 662 | if major(v) == v0v1 { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 663 | return v // +incompatible does not apply for major versions < 2 |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 664 | } |
| 665 | if pc.ModuleExistsAtTaggedVersion(m.Module, v) { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 666 | return v // module@version is already OK |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 667 | } |
| 668 | if vi := v + "+incompatible"; pc.ModuleExistsAtTaggedVersion(m.Module, vi) { |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 669 | return vi |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 670 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 671 | return v // module@version+incompatible doesn't exist |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 672 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 673 | for i, v := range m.Versions { |
| 674 | m.Versions[i].Version = tryAdd(v.Version) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 675 | } |
| 676 | } |
| 677 | |
| 678 | func sortModules(ms []*Module) { |
| 679 | sort.SliceStable(ms, func(i, j int) bool { |
| 680 | m1, m2 := ms[i], ms[j] |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 681 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 682 | // Break ties by versions, assuming the version list is sorted. |
| 683 | // If needed, further break ties by packages. |
| 684 | if m1.Module == m2.Module { |
| 685 | byPackage := func(m1, m2 *Module) bool { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 686 | pkgs1, pkgs2 := m1.Packages, m2.Packages |
| 687 | if len(pkgs1) == 0 { |
| 688 | return true |
| 689 | } else if len(pkgs2) == 0 { |
| 690 | return false |
| 691 | } |
| 692 | return pkgs1[0].Package < pkgs2[0].Package |
| 693 | } |
| 694 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 695 | vr1, vr2 := m1.Versions, m2.Versions |
| 696 | if len(vr1) == 0 && len(vr2) == 0 { |
| 697 | return byPackage(m1, m2) |
| 698 | } else if len(vr1) == 0 { |
| 699 | return true |
| 700 | } else if len(vr2) == 0 { |
| 701 | return false |
| 702 | } |
| 703 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 704 | v1, v2 := vr1[0], vr2[0] |
| 705 | if v1.Version == v2.Version { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 706 | return byPackage(m1, m2) |
| 707 | } |
| 708 | |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 709 | return version.Before(v1.Version, v2.Version) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 710 | } |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 711 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 712 | // Sort by module base name then major version. |
| 713 | base1, major1, ok1 := module.SplitPathVersion(m1.Module) |
| 714 | base2, major2, ok2 := module.SplitPathVersion(m2.Module) |
| 715 | if !ok1 || !ok2 { |
| 716 | return m1.Module < m2.Module |
| 717 | } |
| 718 | |
| 719 | if base1 == base2 { |
| 720 | i1, ok1 := majorToInt(major1) |
| 721 | i2, ok2 := majorToInt(major2) |
| 722 | if ok1 && ok2 { |
| 723 | return i1 < i2 |
| 724 | } |
| 725 | return major1 < major2 |
| 726 | } |
| 727 | |
| 728 | return base1 < base2 |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 729 | }) |
| 730 | } |
| 731 | |
| 732 | // merge merges all modules with the same module & package info |
| 733 | // (but possibly different versions) into one. |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 734 | func merge(ms []*Module) ([]*Module, error) { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 735 | type compMod struct { |
| 736 | path string |
| 737 | packages string // sorted, comma separated list of package names |
| 738 | } |
| 739 | |
| 740 | toCompMod := func(m *Module) compMod { |
| 741 | var packages []string |
| 742 | for _, p := range m.Packages { |
| 743 | packages = append(packages, p.Package) |
| 744 | } |
| 745 | return compMod{ |
| 746 | path: m.Module, |
| 747 | packages: strings.Join(packages, ","), |
| 748 | } |
| 749 | } |
| 750 | |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 751 | // only run if m1 and m2 are same except versions |
| 752 | // deletes vulnerable_at if set |
| 753 | merge := func(m1, m2 *Module) (*Module, error) { |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 754 | merged, err := m1.Versions.mergeStrict(m2.Versions) |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 755 | if err != nil { |
| 756 | return nil, fmt.Errorf("could not merge versions of module %s: %w", m1.Module, err) |
| 757 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 758 | return &Module{ |
| 759 | Module: m1.Module, |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 760 | Versions: merged, |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 761 | UnsupportedVersions: m1.UnsupportedVersions.merge(m2.UnsupportedVersions), |
| 762 | NonGoVersions: m1.NonGoVersions.merge(m2.NonGoVersions), |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 763 | Packages: m1.Packages, |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 764 | }, nil |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 765 | } |
| 766 | |
| 767 | modules := make(map[compMod]*Module) |
| 768 | for _, m := range ms { |
| 769 | c := toCompMod(m) |
| 770 | mod, ok := modules[c] |
| 771 | if !ok { |
| 772 | modules[c] = m |
| 773 | } else { |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 774 | merged, err := merge(mod, m) |
| 775 | if err != nil { |
| 776 | // For now, bail out if any module can't be merged. |
| 777 | // This could be improved by continuing to try even if |
| 778 | // some merges fail. |
| 779 | return nil, err |
| 780 | } |
| 781 | modules[c] = merged |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 782 | } |
| 783 | } |
| 784 | |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 785 | return maps.Values(modules), nil |
| 786 | } |
| 787 | |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 788 | func (v Versions) merge(v2 Versions) Versions { |
| 789 | merged := append(slices.Clone(v), v2...) |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 790 | merged.fix() |
Tatiana Bradley | 68fb04d | 2024-06-24 16:22:00 -0400 | [diff] [blame] | 791 | return merged |
| 792 | } |
| 793 | |
| 794 | func (v Versions) mergeStrict(v2 Versions) (merged Versions, _ error) { |
| 795 | merged = v.merge(v2) |
Tatiana Bradley | f272f63 | 2024-07-08 12:30:31 -0400 | [diff] [blame] | 796 | ranges, err := merged.ToSemverRanges() |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 797 | if err != nil { |
Tatiana Bradley | 925a28e | 2024-05-07 10:33:14 -0400 | [diff] [blame] | 798 | return nil, err |
| 799 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 800 | if err := osvutils.ValidateRanges(ranges); err != nil { |
| 801 | return nil, err |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 802 | } |
Tatiana Bradley | b259823 | 2024-06-21 18:36:29 -0400 | [diff] [blame] | 803 | return merged, nil |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 804 | } |
| 805 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 806 | // FixReferences deletes some unneeded references, and attempts to fix reference types. |
| 807 | // Modifies r. |
| 808 | // |
| 809 | // Deletes: |
| 810 | // - "package"-type references |
| 811 | // - Go advisory references (these are redundant for us) |
| 812 | // - all advisories except the "best" one (if applicable) |
| 813 | // |
| 814 | // Changes: |
| 815 | // - reference type to "advisory" for GHSA and CVE links. |
| 816 | // - reference type to "fix" for Github pull requests and commit links in one of |
| 817 | // the affected modules |
| 818 | // - reference type to "report" for Github issues in one of |
| 819 | // the affected modules |
| 820 | func (r *Report) FixReferences() { |
| 821 | for _, ref := range r.References { |
| 822 | ref.URL = fixURL(ref.URL) |
| 823 | } |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 824 | r.References = slices.DeleteFunc(r.References, func(ref *Reference) bool { |
| 825 | return ref.Type == osv.ReferenceTypePackage || |
| 826 | idstr.IsGoAdvisory(ref.URL) |
| 827 | }) |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 828 | |
| 829 | re := newRE(r) |
| 830 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 831 | aliases := r.Aliases() |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 832 | for _, ref := range r.References { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 833 | switch re.Type(ref.URL, aliases) { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 834 | case urlTypeAdvisory: |
| 835 | ref.Type = osv.ReferenceTypeAdvisory |
| 836 | case urlTypeIssue: |
| 837 | ref.Type = osv.ReferenceTypeReport |
| 838 | case urlTypeFix: |
| 839 | ref.Type = osv.ReferenceTypeFix |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 840 | case urlTypeWeb: |
| 841 | ref.Type = osv.ReferenceTypeWeb |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 842 | } |
| 843 | } |
| 844 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 845 | // If this is a reviewed report, attempt to find the "best" advisory and delete others. |
| 846 | if r.IsReviewed() { |
| 847 | if bestAdvisory := bestAdvisory(r.References, r.Aliases()); bestAdvisory != "" { |
| 848 | isNotBest := func(ref *Reference) bool { |
| 849 | return ref.Type == osv.ReferenceTypeAdvisory && ref.URL != bestAdvisory |
| 850 | } |
| 851 | r.References = slices.DeleteFunc(r.References, isNotBest) |
| 852 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 853 | } |
| 854 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 855 | if r.countAdvisories() == 0 && r.needsAdvisory() { |
| 856 | if r.hasExternalSource() { |
| 857 | r.addSourceAdvisory() |
| 858 | } else if as := r.Aliases(); len(as) > 0 { |
| 859 | r.addAdvisory(as[0]) |
| 860 | } |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 861 | } |
| 862 | |
Tatiana Bradley | 4191954 | 2024-05-15 13:45:16 -0400 | [diff] [blame] | 863 | slices.SortFunc(r.References, func(a *Reference, b *Reference) int { |
| 864 | if a.Type == b.Type { |
| 865 | return strings.Compare(a.URL, b.URL) |
| 866 | } |
| 867 | return strings.Compare(string(a.Type), string(b.Type)) |
| 868 | }) |
| 869 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 870 | if len(r.References) == 0 { |
| 871 | r.References = nil |
| 872 | } |
| 873 | } |
| 874 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 875 | func (r *Report) hasExternalSource() bool { |
| 876 | return r.SourceMeta != nil && idstr.IsIdentifier(r.SourceMeta.ID) |
| 877 | } |
| 878 | |
| 879 | func (r *Report) addAdvisory(id string) { |
| 880 | if link := idstr.AdvisoryLink(id); link != "" { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 881 | r.References = append(r.References, &Reference{ |
| 882 | Type: osv.ReferenceTypeAdvisory, |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 883 | URL: link, |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 884 | }) |
| 885 | } |
| 886 | } |
| 887 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 888 | func (r *Report) addSourceAdvisory() { |
| 889 | srcID := r.SourceMeta.ID |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 890 | for _, ref := range r.References { |
Tatiana Bradley | 42ebd52 | 2024-06-21 11:55:18 -0400 | [diff] [blame] | 891 | if idstr.IsAdvisoryFor(ref.URL, srcID) { |
| 892 | ref.Type = osv.ReferenceTypeAdvisory |
| 893 | return |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 894 | } |
| 895 | } |
Tatiana Bradley | 42ebd52 | 2024-06-21 11:55:18 -0400 | [diff] [blame] | 896 | r.addAdvisory(srcID) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 897 | } |
| 898 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 899 | // bestAdvisory returns the URL of the "best" advisory in the references, |
| 900 | // or ("", false) if none can be found. |
| 901 | // Repository-level GHSAs are considered the best, followed by regular |
| 902 | // GHSAs, followed by CVEs. |
| 903 | // For now, if there are advisories mentioning two or more |
| 904 | // aliases of the same type, we don't try to determine which is best. |
| 905 | // (For example, if there are two advisories, referencing GHSA-1 and GHSA-2, we leave it |
| 906 | // to the triager to pick the best one.) |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 907 | func bestAdvisory(refs []*Reference, aliases []string) string { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 908 | bestAdvisory := "" |
| 909 | bestType := advisoryTypeUnknown |
| 910 | ghsas, cves := make(map[string]bool), make(map[string]bool) |
| 911 | for _, ref := range refs { |
| 912 | if ref.Type != osv.ReferenceTypeAdvisory { |
| 913 | continue |
| 914 | } |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 915 | alias, ok := idstr.IsAdvisoryForOneOf(ref.URL, aliases) |
| 916 | if !ok { |
| 917 | continue |
| 918 | } |
| 919 | if t := advisoryTypeOf(ref.URL); t > bestType { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 920 | bestAdvisory = ref.URL |
| 921 | bestType = t |
| 922 | } |
| 923 | |
Tatiana Bradley | 685ac19 | 2024-04-22 13:35:43 -0400 | [diff] [blame] | 924 | if idstr.IsGHSA(alias) { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 925 | ghsas[alias] = true |
Tatiana Bradley | 685ac19 | 2024-04-22 13:35:43 -0400 | [diff] [blame] | 926 | } else if idstr.IsCVE(alias) { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 927 | cves[alias] = true |
| 928 | } |
| 929 | } |
| 930 | |
| 931 | if len(ghsas) > 1 || len(cves) > 1 { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 932 | return "" |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 933 | } |
| 934 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 935 | return bestAdvisory |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 936 | } |
| 937 | |
| 938 | type urlType int |
| 939 | |
| 940 | const ( |
| 941 | urlTypeUnknown urlType = iota |
| 942 | urlTypeIssue |
| 943 | urlTypeFix |
| 944 | urlTypeAdvisory |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 945 | urlTypeWeb |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 946 | ) |
| 947 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 948 | func (re *reportRE) Type(url string, aliases []string) urlType { |
| 949 | if _, ok := idstr.IsAdvisoryForOneOf(url, aliases); ok { |
| 950 | return urlTypeAdvisory |
| 951 | } else if idstr.IsAdvisory(url) { |
| 952 | // URLs that point to other vulns should not be considered |
| 953 | // advisories for this vuln. |
| 954 | return urlTypeWeb |
| 955 | } |
| 956 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 957 | switch { |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 958 | case re.issue.MatchString(url): |
| 959 | return urlTypeIssue |
| 960 | case re.fix.MatchString(url): |
| 961 | return urlTypeFix |
| 962 | } |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 963 | |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 964 | return urlTypeUnknown |
| 965 | } |
| 966 | |
| 967 | type advisoryType int |
| 968 | |
| 969 | // Advisory link types in ascending order of (likely) quality. |
| 970 | // In general, repo-level GHSAs tend to be the best because |
| 971 | // they are more likely to be directly created by a maintainer. |
| 972 | const ( |
| 973 | advisoryTypeUnknown advisoryType = iota |
| 974 | advisoryTypeCVE |
| 975 | advisoryTypeGHSA |
| 976 | advisoryTypeGHSARepo |
| 977 | ) |
| 978 | |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 979 | func advisoryTypeOf(url string) advisoryType { |
| 980 | switch { |
| 981 | case idstr.IsCVELink(url): |
| 982 | return advisoryTypeCVE |
| 983 | case idstr.IsGHSAGlobalLink(url): |
| 984 | return advisoryTypeGHSA |
| 985 | case idstr.IsGHSARepoLink(url): |
| 986 | return advisoryTypeGHSARepo |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 987 | } |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 988 | return advisoryTypeUnknown |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 989 | } |
| 990 | |
| 991 | type reportRE struct { |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 992 | issue, fix *regexp.Regexp |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 993 | } |
| 994 | |
| 995 | func newRE(r *Report) *reportRE { |
| 996 | oneOfRE := func(s []string) string { |
| 997 | return `(` + strings.Join(s, "|") + `)` |
| 998 | } |
| 999 | |
| 1000 | // For now, this will not attempt to fix reference types for |
| 1001 | // modules whose canonical names are different from their github path. |
| 1002 | var modulePaths []string |
| 1003 | for _, m := range r.Modules { |
| 1004 | modulePaths = append(modulePaths, m.Module) |
| 1005 | } |
| 1006 | moduleRE := oneOfRE(modulePaths) |
| 1007 | |
| 1008 | return &reportRE{ |
Tatiana Bradley | 2122bde | 2024-05-03 14:21:46 -0400 | [diff] [blame] | 1009 | issue: regexp.MustCompile(`^https://` + moduleRE + `/issue(s?)/.*$`), |
| 1010 | fix: regexp.MustCompile(`^https://` + moduleRE + `/(commit(s?)|pull)/.*$`), |
Tatiana Bradley | c387d46 | 2024-04-17 17:08:10 -0400 | [diff] [blame] | 1011 | } |
| 1012 | } |