blob: 2c1fb3706f4022f6b03465232a7602e64ddeb54a [file] [log] [blame]
Tatiana Bradley38167212023-05-22 13:59:47 -04001// 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
5package report
6
7import (
Tatiana Bradley1468b952023-06-22 17:25:14 -04008 "errors"
9 "fmt"
Tatiana Bradley38167212023-05-22 13:59:47 -040010 "regexp"
Tatiana Bradley4c0120a2023-06-15 15:09:12 -040011 "sort"
Tatiana Bradley68fb04d2024-06-24 16:22:00 -040012 "strconv"
Tatiana Bradley23bd6a82023-06-21 15:43:00 -040013 "strings"
Tatiana Bradley38167212023-05-22 13:59:47 -040014
Tatiana Bradleyc387d462024-04-17 17:08:10 -040015 "golang.org/x/exp/maps"
Tatiana Bradley3bd4bef2023-09-18 17:09:17 -040016 "golang.org/x/exp/slices"
Tatiana Bradleyc387d462024-04-17 17:08:10 -040017 "golang.org/x/mod/module"
Tatiana Bradley685ac192024-04-22 13:35:43 -040018 "golang.org/x/vulndb/internal/idstr"
Tatiana Bradley3754b2a2024-02-07 12:23:17 -050019 "golang.org/x/vulndb/internal/osv"
Tatiana Bradley925a28e2024-05-07 10:33:14 -040020 "golang.org/x/vulndb/internal/osvutils"
Tatiana Bradley38167212023-05-22 13:59:47 -040021 "golang.org/x/vulndb/internal/proxy"
Tatiana Bradley68fb04d2024-06-24 16:22:00 -040022 "golang.org/x/vulndb/internal/stdlib"
Tatiana Bradley38167212023-05-22 13:59:47 -040023 "golang.org/x/vulndb/internal/version"
24)
25
Tatiana Bradley24e908f2023-08-29 14:10:18 -040026func (r *Report) Fix(pc *proxy.Client) {
Tatiana Bradley2122bde2024-05-03 14:21:46 -040027 r.deleteNotes(NoteTypeFix)
Tim King878e33a2024-03-21 13:50:11 -070028 expandGitCommits(r)
Tatiana Bradley68fb04d2024-06-24 16:22:00 -040029 _ = r.FixModules(pc)
Tatiana Bradley0c44de62024-02-06 13:57:48 -050030 r.FixText()
Tatiana Bradley3754b2a2024-02-07 12:23:17 -050031 r.FixReferences()
Tatiana Bradley0c44de62024-02-06 13:57:48 -050032}
33
34func (r *Report) FixText() {
Tatiana Bradley23bd6a82023-06-21 15:43:00 -040035 fixLines := func(sp *string) {
36 *sp = fixLineLength(*sp, maxLineLength)
37 }
Tatiana Bradley79523f12023-11-14 11:10:14 -050038 fixLines((*string)(&r.Summary))
Tatiana Bradley11844d72023-11-14 11:19:03 -050039 fixLines((*string)(&r.Description))
Tatiana Bradley23bd6a82023-06-21 15:43:00 -040040 if r.CVEMetadata != nil {
41 fixLines(&r.CVEMetadata.Description)
42 }
Tatiana Bradleybbf0d712024-04-05 12:52:03 -040043
44 r.fixSummary()
45}
46
47func (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 Bradleye5d28b92024-07-23 16:58:12 -040061 summary = fmt.Sprintf("%s in %s", summary, stripMajor(paths[0]))
Tatiana Bradleybbf0d712024-04-05 12:52:03 -040062 }
63
Tatiana Bradley85d976c2024-06-26 16:28:58 -040064 r.Summary = Summary(fixSpelling(summary))
65}
66
Tatiana Bradleye5d28b92024-07-23 16:58:12 -040067func stripMajor(path string) string {
68 base, _, ok := module.SplitPathVersion(path)
69 if !ok {
70 return path
71 }
72 return base
73}
74
Tatiana Bradleyb2598232024-06-21 18:36:29 -040075func (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 Bradleyc7795302023-08-02 15:17:35 -040088// FixVersions replaces each version with its canonical form (if possible),
Tatiana Bradley68fb04d2024-06-24 16:22:00 -040089// sorts version ranges, and moves versions to their proper spot.
Tatiana Bradley24e908f2023-08-29 14:10:18 -040090func (m *Module) FixVersions(pc *proxy.Client) {
Tatiana Bradleyb2598232024-06-21 18:36:29 -040091 for _, v := range m.Versions {
92 v.commitHashToVersion(m.Module, pc)
Tatiana Bradley38167212023-05-22 13:59:47 -040093 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -040094 m.VulnerableAt.commitHashToVersion(m.Module, pc)
Tatiana Bradley4c0120a2023-06-15 15:09:12 -040095
Tatiana Bradleyb2598232024-06-21 18:36:29 -040096 m.Versions.fix()
Tatiana Bradley68fb04d2024-06-24 16:22:00 -040097 m.UnsupportedVersions.fix()
Tatiana Bradleyb2598232024-06-21 18:36:29 -040098 m.VulnerableAt.fix()
Tatiana Bradley4c0120a2023-06-15 15:09:12 -040099
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400100 if pc != nil && !m.IsFirstParty() {
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400101 found, notFound, _ := m.classifyVersions(pc)
102 if len(notFound) != 0 {
103 m.Versions = found
104 m.NonGoVersions = append(m.NonGoVersions, notFound...)
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400105 }
106 }
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400107}
108
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400109func (v *Version) fix() {
110 if v == nil {
111 return
Tatiana Bradley38167212023-05-22 13:59:47 -0400112 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400113 vv := version.TrimPrefix(v.Version)
114 if version.IsValid(vv) {
115 vv = version.Canonical(vv)
116 }
117 v.Version = vv
Tatiana Bradley95f52fc2023-08-28 16:02:46 -0400118}
119
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400120func (vs *Versions) fix() {
121 for i := range *vs {
122 (*vs)[i].fix()
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400123 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400124 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 Bradley2122bde2024-05-03 14:21:46 -0400132}
133
134func (m *Module) fixVulnerableAt(pc *proxy.Client) error {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400135 if m.VulnerableAt != nil {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400136 return nil
137 }
138 if m.IsFirstParty() {
139 return fmt.Errorf("not implemented for std/cmd")
Tatiana Bradley1468b952023-06-22 17:25:14 -0400140 }
Tatiana Bradley95f52fc2023-08-28 16:02:46 -0400141 // Don't attempt to guess if the given version ranges don't make sense.
Tatiana Bradley477a0752023-09-21 14:11:05 -0400142 if err := m.checkModVersions(pc); err != nil {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400143 return err
Tatiana Bradley95f52fc2023-08-28 16:02:46 -0400144 }
Tatiana Bradley24e908f2023-08-29 14:10:18 -0400145 v, err := m.guessVulnerableAt(pc)
Tatiana Bradley95f52fc2023-08-28 16:02:46 -0400146 if err != nil {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400147 return err
Tatiana Bradley95f52fc2023-08-28 16:02:46 -0400148 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400149 m.VulnerableAt = VulnerableAt(v)
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400150 return nil
Tatiana Bradley1468b952023-06-22 17:25:14 -0400151}
152
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400153var 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.
157func (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 Bradley1468b952023-06-22 17:25:14 -0400175// guessVulnerableAt attempts to find a vulnerable_at
Tatiana Bradley95f52fc2023-08-28 16:02:46 -0400176// version using the module proxy, assuming that the version ranges
177// have already been validated.
Tatiana Bradley1468b952023-06-22 17:25:14 -0400178// If there is no fix, the latest version is used.
Tatiana Bradley8ab45182023-08-30 15:04:06 -0400179func (m *Module) guessVulnerableAt(pc *proxy.Client) (v string, err error) {
Tatiana Bradley1468b952023-06-22 17:25:14 -0400180 if m.IsFirstParty() {
181 return "", errors.New("cannot auto-guess vulnerable_at for first-party modules")
182 }
183
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400184 introduced, fixed := m.Versions.latestVersions()
Tatiana Bradley1468b952023-06-22 17:25:14 -0400185
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400186 // If there is no latest fix, find the latest version of the module.
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400187 if fixed == nil {
Tatiana Bradley1468b952023-06-22 17:25:14 -0400188 latest, err := pc.Latest(m.Module)
189 if err != nil || latest == "" {
Tatiana Bradley207b5b92023-08-23 15:12:16 -0400190 return "", fmt.Errorf("no fix, but could not find latest version from proxy: %s", err)
191 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400192 if introduced != nil && version.Before(latest, introduced.Version) {
193 return "", fmt.Errorf("latest version (%s) is before last introduced version", latest)
194 }
Tatiana Bradley1468b952023-06-22 17:25:14 -0400195 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 Bradleyb2598232024-06-21 18:36:29 -0400200 if !version.IsValid(fixed.Version) {
Tatiana Bradley1468b952023-06-22 17:25:14 -0400201 return "", errors.New("cannot auto-guess when fixed version is invalid")
202 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400203 if strings.HasPrefix(fixed.Version, "0.0.0-") {
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400204 return "", errZeroPseudo
Tatiana Bradley1468b952023-06-22 17:25:14 -0400205 }
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 Bradleyb2598232024-06-21 18:36:29 -0400213 if version.Before(vs[i], fixed.Version) {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400214 // Make sure the version is >= the latest introduced version.
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400215 if introduced == nil || !version.Before(vs[i], introduced.Version) {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400216 return vs[i], nil
217 }
Tatiana Bradley1468b952023-06-22 17:25:14 -0400218 }
219 }
220
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400221 return "", errors.New("could not find tagged version between introduced and fixed")
Tatiana Bradley38167212023-05-22 13:59:47 -0400222}
223
Tatiana Bradley23bd6a82023-06-21 15:43:00 -0400224// 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.
228func 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 Bradley38167212023-05-22 13:59:47 -0400266var 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
285func fixURL(u string) string {
286 for _, repl := range urlReplacements {
287 u = repl.re.ReplaceAllString(u, repl.repl)
288 }
289 return u
290}
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400291
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400292func (r *Report) FixModules(pc *proxy.Client) (errs error) {
293 var fixed []*Module
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400294 for _, m := range r.Modules {
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400295 m.Module = transform(m.Module)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400296 extractImportPath(m, pc)
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400297 fixed = append(fixed, m.splitByMajor(pc)...)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400298 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400299 r.Modules = fixed
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400300
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400301 merged, err := merge(fixed)
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400302 if err != nil {
303 r.AddNote(NoteTypeFix, "module merge error: %s", err)
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400304 errs = errors.Join(errs, err)
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400305 } else {
306 r.Modules = merged
307 }
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400308
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400309 // For unreviewed reports, assume that all major versions
310 // up to the highest mentioned are affected at all versions.
Tatiana Bradleybca6ae22024-07-15 16:36:44 -0400311 if r.IsUnreviewed() {
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400312 r.addMissingMajors(pc)
313 }
314
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400315 // Fix the versions *after* the modules have been merged.
316 for _, m := range r.Modules {
317 m.FixVersions(pc)
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400318 if err := m.fixVulnerableAt(pc); err != nil {
319 r.AddNote(NoteTypeFix, "%s: could not add vulnerable_at: %v", m.Module, err)
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400320 errs = errors.Join(errs, err)
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400321 }
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400322 }
323
324 sortModules(r.Modules)
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400325 return errs
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400326}
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.
334func 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 Bradley68fb04d2024-06-24 16:22:00 -0400345func (m *Module) hasVersions() bool {
346 return len(m.Versions) != 0 || len(m.NonGoVersions) != 0 || len(m.UnsupportedVersions) != 0
347}
348
349type majorInfo struct {
350 base string
351 high int
352 all map[int]bool
353}
354
355func majorToInt(maj string) (int, bool) {
356 if maj == "" {
357 return 0, true
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400358 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400359 i, err := strconv.Atoi(strings.TrimPrefix(maj, "/v"))
360 if err != nil {
361 return 0, false
362 }
363 return i, true
364}
365
366func intToMajor(i int) string {
367 if i == 0 {
368 return v0v1
369 }
370 return fmt.Sprintf("v%d", i)
371}
372
373func (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 Bradleyc387d462024-04-17 17:08:10 -0400390 }
391 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400392 if i > majorMap[v1Mod].high {
393 majorMap[v1Mod].high = i
394 }
395 majorMap[v1Mod].all[i] = true
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400396 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400397
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 Bradleyc387d462024-04-17 17:08:10 -0400411 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400412}
413
414func (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 Bradleyc387d462024-04-17 17:08:10 -0400419 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400420
421 base, _, ok := module.SplitPathVersion(m.Module)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400422 if !ok { // couldn't parse module path, don't attempt to fix
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400423 return []*Module{m}
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400424 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400425 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 Bradleyc387d462024-04-17 17:08:10 -0400441 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400442
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 Bradleyc387d462024-04-17 17:08:10 -0400451 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400452
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
470var 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
476func transform(m string) string {
477 if t, ok := transforms[m]; ok {
478 return t
479 }
480 return m
481}
482
483func 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
493func (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
506func (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
517func (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
527func 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
538func (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 Bradleyebcb2442024-07-17 17:17:22 -0400549 SkipFixSymbols: p.SkipFixSymbols,
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400550 }
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400551}
552
553const (
554 v0 = "v0"
555 v1 = "v1"
556 v0v1 = "v0 or v1"
557)
558
559func 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 Bradley68fb04d2024-06-24 16:22:00 -0400567type allVersions struct {
568 standard, unsupported, nonGo Versions
569}
570
571func (a *allVersions) add(b *allVersions) {
572 if b == nil {
573 return
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400574 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400575 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
580func (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 Bradleyc387d462024-04-17 17:08:10 -0400586 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400587 return maj
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400588 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400589 for _, v := range m.Versions {
590 maj := getMajor(v)
591 mp[maj].standard = append(mp[maj].standard, v)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400592 }
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400593 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 Bradleyc387d462024-04-17 17:08:10 -0400602}
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.
610func 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
631func commonCanonical(m *Module, pc *proxy.Client) (string, error) {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400632 if len(m.Versions) == 0 {
633 return m.Module, nil
634 }
635
636 canonical, err := pc.CanonicalModulePath(m.Module, m.Versions[0].Version)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400637 if err != nil {
638 return "", err
639 }
640
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400641 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 Bradleyc387d462024-04-17 17:08:10 -0400648 }
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).
657func addIncompatible(m *Module, pc *proxy.Client) {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400658 tryAdd := func(v string) string {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400659 if v == "" {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400660 return v
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400661 }
662 if major(v) == v0v1 {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400663 return v // +incompatible does not apply for major versions < 2
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400664 }
665 if pc.ModuleExistsAtTaggedVersion(m.Module, v) {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400666 return v // module@version is already OK
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400667 }
668 if vi := v + "+incompatible"; pc.ModuleExistsAtTaggedVersion(m.Module, vi) {
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400669 return vi
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400670 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400671 return v // module@version+incompatible doesn't exist
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400672 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400673 for i, v := range m.Versions {
674 m.Versions[i].Version = tryAdd(v.Version)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400675 }
676}
677
678func sortModules(ms []*Module) {
679 sort.SliceStable(ms, func(i, j int) bool {
680 m1, m2 := ms[i], ms[j]
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400681
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400682 // 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 Bradleyc387d462024-04-17 17:08:10 -0400686 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 Bradley2122bde2024-05-03 14:21:46 -0400695 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 Bradleyb2598232024-06-21 18:36:29 -0400704 v1, v2 := vr1[0], vr2[0]
705 if v1.Version == v2.Version {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400706 return byPackage(m1, m2)
707 }
708
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400709 return version.Before(v1.Version, v2.Version)
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400710 }
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400711
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400712 // 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 Bradleyc387d462024-04-17 17:08:10 -0400729 })
730}
731
732// merge merges all modules with the same module & package info
733// (but possibly different versions) into one.
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400734func merge(ms []*Module) ([]*Module, error) {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400735 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 Bradley925a28e2024-05-07 10:33:14 -0400751 // 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 Bradley68fb04d2024-06-24 16:22:00 -0400754 merged, err := m1.Versions.mergeStrict(m2.Versions)
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400755 if err != nil {
756 return nil, fmt.Errorf("could not merge versions of module %s: %w", m1.Module, err)
757 }
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400758 return &Module{
759 Module: m1.Module,
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400760 Versions: merged,
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400761 UnsupportedVersions: m1.UnsupportedVersions.merge(m2.UnsupportedVersions),
762 NonGoVersions: m1.NonGoVersions.merge(m2.NonGoVersions),
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400763 Packages: m1.Packages,
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400764 }, nil
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400765 }
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 Bradley925a28e2024-05-07 10:33:14 -0400774 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 Bradleyc387d462024-04-17 17:08:10 -0400782 }
783 }
784
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400785 return maps.Values(modules), nil
786}
787
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400788func (v Versions) merge(v2 Versions) Versions {
789 merged := append(slices.Clone(v), v2...)
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400790 merged.fix()
Tatiana Bradley68fb04d2024-06-24 16:22:00 -0400791 return merged
792}
793
794func (v Versions) mergeStrict(v2 Versions) (merged Versions, _ error) {
795 merged = v.merge(v2)
Tatiana Bradleyf272f632024-07-08 12:30:31 -0400796 ranges, err := merged.ToSemverRanges()
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400797 if err != nil {
Tatiana Bradley925a28e2024-05-07 10:33:14 -0400798 return nil, err
799 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400800 if err := osvutils.ValidateRanges(ranges); err != nil {
801 return nil, err
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400802 }
Tatiana Bradleyb2598232024-06-21 18:36:29 -0400803 return merged, nil
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400804}
805
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400806// 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
820func (r *Report) FixReferences() {
821 for _, ref := range r.References {
822 ref.URL = fixURL(ref.URL)
823 }
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400824 r.References = slices.DeleteFunc(r.References, func(ref *Reference) bool {
825 return ref.Type == osv.ReferenceTypePackage ||
826 idstr.IsGoAdvisory(ref.URL)
827 })
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400828
829 re := newRE(r)
830
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400831 aliases := r.Aliases()
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400832 for _, ref := range r.References {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400833 switch re.Type(ref.URL, aliases) {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400834 case urlTypeAdvisory:
835 ref.Type = osv.ReferenceTypeAdvisory
836 case urlTypeIssue:
837 ref.Type = osv.ReferenceTypeReport
838 case urlTypeFix:
839 ref.Type = osv.ReferenceTypeFix
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400840 case urlTypeWeb:
841 ref.Type = osv.ReferenceTypeWeb
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400842 }
843 }
844
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400845 // 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 Bradleyc387d462024-04-17 17:08:10 -0400853 }
854
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400855 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 Bradleyc387d462024-04-17 17:08:10 -0400861 }
862
Tatiana Bradley41919542024-05-15 13:45:16 -0400863 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 Bradleyc387d462024-04-17 17:08:10 -0400870 if len(r.References) == 0 {
871 r.References = nil
872 }
873}
874
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400875func (r *Report) hasExternalSource() bool {
876 return r.SourceMeta != nil && idstr.IsIdentifier(r.SourceMeta.ID)
877}
878
879func (r *Report) addAdvisory(id string) {
880 if link := idstr.AdvisoryLink(id); link != "" {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400881 r.References = append(r.References, &Reference{
882 Type: osv.ReferenceTypeAdvisory,
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400883 URL: link,
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400884 })
885 }
886}
887
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400888func (r *Report) addSourceAdvisory() {
889 srcID := r.SourceMeta.ID
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400890 for _, ref := range r.References {
Tatiana Bradley42ebd522024-06-21 11:55:18 -0400891 if idstr.IsAdvisoryFor(ref.URL, srcID) {
892 ref.Type = osv.ReferenceTypeAdvisory
893 return
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400894 }
895 }
Tatiana Bradley42ebd522024-06-21 11:55:18 -0400896 r.addAdvisory(srcID)
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400897}
898
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400899// 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 Bradley2122bde2024-05-03 14:21:46 -0400907func bestAdvisory(refs []*Reference, aliases []string) string {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400908 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 Bradley2122bde2024-05-03 14:21:46 -0400915 alias, ok := idstr.IsAdvisoryForOneOf(ref.URL, aliases)
916 if !ok {
917 continue
918 }
919 if t := advisoryTypeOf(ref.URL); t > bestType {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400920 bestAdvisory = ref.URL
921 bestType = t
922 }
923
Tatiana Bradley685ac192024-04-22 13:35:43 -0400924 if idstr.IsGHSA(alias) {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400925 ghsas[alias] = true
Tatiana Bradley685ac192024-04-22 13:35:43 -0400926 } else if idstr.IsCVE(alias) {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400927 cves[alias] = true
928 }
929 }
930
931 if len(ghsas) > 1 || len(cves) > 1 {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400932 return ""
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400933 }
934
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400935 return bestAdvisory
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400936}
937
938type urlType int
939
940const (
941 urlTypeUnknown urlType = iota
942 urlTypeIssue
943 urlTypeFix
944 urlTypeAdvisory
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400945 urlTypeWeb
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400946)
947
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400948func (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 Bradleyc387d462024-04-17 17:08:10 -0400957 switch {
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400958 case re.issue.MatchString(url):
959 return urlTypeIssue
960 case re.fix.MatchString(url):
961 return urlTypeFix
962 }
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400963
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400964 return urlTypeUnknown
965}
966
967type 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.
972const (
973 advisoryTypeUnknown advisoryType = iota
974 advisoryTypeCVE
975 advisoryTypeGHSA
976 advisoryTypeGHSARepo
977)
978
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400979func 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 Bradleyc387d462024-04-17 17:08:10 -0400987 }
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400988 return advisoryTypeUnknown
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400989}
990
991type reportRE struct {
Tatiana Bradley2122bde2024-05-03 14:21:46 -0400992 issue, fix *regexp.Regexp
Tatiana Bradleyc387d462024-04-17 17:08:10 -0400993}
994
995func 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 Bradley2122bde2024-05-03 14:21:46 -04001009 issue: regexp.MustCompile(`^https://` + moduleRE + `/issue(s?)/.*$`),
1010 fix: regexp.MustCompile(`^https://` + moduleRE + `/(commit(s?)|pull)/.*$`),
Tatiana Bradleyc387d462024-04-17 17:08:10 -04001011 }
1012}