internal/{genericosv,report}: move shared logic to report.New

Move logic that makes sense to be performed on all new reports,
such as fixing references and modules, to report.New.

Some of this logic was previously only applied to GHSAs (but
should have also been applied to CVEs), so this does affect
test outputs.

Change-Id: I8e43abe385fda4cdef5c1818c701cbb83e1c403b
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/579859
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/genericosv/report.go b/internal/genericosv/report.go
index 4bea906..dc76a6a 100644
--- a/internal/genericosv/report.go
+++ b/internal/genericosv/report.go
@@ -6,33 +6,22 @@
 
 import (
 	"fmt"
-	"regexp"
-	"sort"
 	"strings"
 
 	osvschema "github.com/google/osv-scanner/pkg/models"
-	"golang.org/x/exp/maps"
-	"golang.org/x/exp/slices"
-	"golang.org/x/mod/module"
 	"golang.org/x/vulndb/internal/cveschema5"
 	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/osv"
-	"golang.org/x/vulndb/internal/proxy"
 	"golang.org/x/vulndb/internal/report"
-	"golang.org/x/vulndb/internal/version"
 )
 
 var _ report.Source = &Entry{}
 
 // ToReport converts OSV into a Go Report with the given ID.
-func (osv *Entry) ToReport(goID string, _ string, pc *proxy.Client) *report.Report {
+func (osv *Entry) ToReport(string) *report.Report {
 	r := &report.Report{
-		ID:          goID,
 		Summary:     report.Summary(osv.Summary),
 		Description: report.Description(osv.Details),
-		SourceMeta: &report.SourceMeta{
-			ID: osv.ID,
-		},
 	}
 	addAlias := func(alias string) {
 		switch {
@@ -49,16 +38,13 @@
 		addAlias(alias)
 	}
 
-	r.Modules = affectedToModules(osv.Affected, pc)
+	r.Modules = affectedToModules(osv.Affected)
 
 	for _, ref := range osv.References {
 		r.References = append(r.References, convertRef(ref))
 	}
-	fixRefs(r)
 
 	r.Credits = convertCredits(osv.Credits)
-
-	r.Fix(pc)
 	return r
 }
 
@@ -66,7 +52,7 @@
 	return osv.ID
 }
 
-func affectedToModules(as []osvschema.Affected, pc *proxy.Client) []*report.Module {
+func affectedToModules(as []osvschema.Affected) []*report.Module {
 	var modules []*report.Module
 	for _, a := range as {
 		if a.Package.Ecosystem != osvschema.EcosystemGo {
@@ -80,291 +66,9 @@
 			UnsupportedVersions: unsupportedVersions,
 		})
 	}
-
-	for _, m := range modules {
-		extractImportPath(m, pc)
-		if ok := fixMajorVersion(m, pc); !ok {
-			addIncompatible(m, pc)
-		}
-		canonicalize(m, pc)
-	}
-
-	modules = merge(modules)
-
-	// Fix the versions *after* the modules have been merged.
-	for _, m := range modules {
-		m.FixVersions(pc)
-	}
-
-	sortModules(modules)
 	return modules
 }
 
-// extractImportPath checks if the module m's "module" path is actually
-// an import path. If so, it adds the import path to the packages list
-// and fixes the module path. Modifies m.
-//
-// Does nothing if the module path is already correct, or isn't recognized
-// by the proxy at all.
-func extractImportPath(m *report.Module, pc *proxy.Client) {
-	path := m.Module
-	modulePath, err := pc.FindModule(m.Module)
-	if err != nil || // path doesn't contain a module, needs human review
-		path == modulePath { // path is already a module, no action needed
-		return
-	}
-	m.Module = modulePath
-	m.Packages = append(m.Packages, &report.Package{Package: path})
-}
-
-// fixMajorVersion corrects the major version prefix of the module
-// path if possible.
-// Returns true if the major version was already correct or could be
-// fixed.
-// For now, it gives up if it encounters various problems and
-// special cases (see comments inline).
-func fixMajorVersion(m *report.Module, pc *proxy.Client) (ok bool) {
-	if strings.HasPrefix(m.Module, "gopkg.in/") {
-		return false // don't attempt to fix gopkg.in modules
-	}
-	// If there is no "introduced" version, don't attempt to fix
-	// major version.
-	// Example: example.com/module is fixed at 2.2.2. This likely means
-	// that example.com/module is vulnerable at all versions and
-	// example.com/module/v2 is vulnerable up to 2.2.2.
-	// Changing example.com/module to example.com/module/v2 would lose
-	// information.
-	hasIntroduced := func(m *report.Module) bool {
-		for _, vr := range m.Versions {
-			if vr.Introduced != "" {
-				return true
-			}
-		}
-		return false
-	}
-	if !hasIntroduced(m) {
-		return false
-	}
-	wantMajor, ok := commonMajor(m.Versions)
-	if !ok { // inconsistent major version, don't attempt to fix
-		return false
-	}
-	prefix, major, ok := module.SplitPathVersion(m.Module)
-	if !ok { // couldn't parse module path, don't attempt to fix
-		return false
-	}
-	if major == wantMajor {
-		return true // nothing to do
-	}
-	fixed := prefix + wantMajor
-	if !pc.ModuleExists(fixed) {
-		return false // attempted fixed module doesn't exist, give up
-	}
-	m.Module = fixed
-	return true
-}
-
-const (
-	v0   = "v0"
-	v1   = "v1"
-	v0v1 = "v0 or v1"
-)
-
-func major(v string) string {
-	m := version.Major(v)
-	if m == v0 || m == v1 {
-		return v0v1
-	}
-	return m
-}
-
-// commonMajor returns the major version path suffix (e.g. "/v2") common
-// to all versions in the version range, or ("", false) if not all versions
-// have the same major version.
-// Returns ("", true) if the major version is 0 or 1.
-func commonMajor(vs []report.VersionRange) (_ string, ok bool) {
-	maj := major(first(vs))
-	for _, vr := range vs {
-		for _, v := range []string{vr.Introduced, vr.Fixed} {
-			if v == "" {
-				continue
-			}
-			current := major(v)
-			if current != maj {
-				return "", false
-			}
-		}
-	}
-	if maj == v0v1 {
-		return "", true
-	}
-	return "/" + maj, true
-}
-
-// canonicalize attempts to canonicalize the module path,
-// and updates the module path and packages list if successful.
-// Modifies m.
-//
-// Does nothing if the module path is already canonical, or isn't recognized
-// by the proxy at all.
-func canonicalize(m *report.Module, pc *proxy.Client) {
-	if len(m.Versions) == 0 {
-		return // no versions, don't attempt to fix
-	}
-
-	canonical, err := commonCanonical(m, pc)
-	if err != nil {
-		return // no consistent canonical version found, don't attempt to fix
-	}
-
-	original := m.Module
-	m.Module = canonical
-
-	// Fix any package paths.
-	for _, p := range m.Packages {
-		if strings.HasPrefix(p.Package, original) {
-			p.Package = canonical + strings.TrimPrefix(p.Package, original)
-		}
-	}
-}
-
-func commonCanonical(m *report.Module, pc *proxy.Client) (string, error) {
-	canonical, err := pc.CanonicalModulePath(m.Module, first(m.Versions))
-	if err != nil {
-		return "", err
-	}
-
-	for _, vr := range m.Versions {
-		for _, v := range []string{vr.Introduced, vr.Fixed} {
-			if v == "" {
-				continue
-			}
-			current, err := pc.CanonicalModulePath(m.Module, v)
-			if err != nil {
-				return "", err
-			}
-			if current != canonical {
-				return "", fmt.Errorf("inconsistent canonical module paths: %s and %s", canonical, current)
-			}
-		}
-	}
-	return canonical, nil
-}
-
-// addIncompatible adds "+incompatible" to all versions where module@version
-// does not exist but module@version+incompatible does exist.
-// TODO(https://go.dev/issue/61769): Consider making this work for
-// non-canonical versions too (example: GHSA-w4xh-w33p-4v29).
-func addIncompatible(m *report.Module, pc *proxy.Client) {
-	tryAdd := func(v string) (string, bool) {
-		if v == "" {
-			return "", false
-		}
-		if major(v) == v0v1 {
-			return "", false // +incompatible does not apply for major versions < 2
-		}
-		if pc.ModuleExistsAtTaggedVersion(m.Module, v) {
-			return "", false // module@version is already OK
-		}
-		if vi := v + "+incompatible"; pc.ModuleExistsAtTaggedVersion(m.Module, vi) {
-			return vi, true
-		}
-		return "", false // module@version+incompatible doesn't exist
-	}
-	for i, vr := range m.Versions {
-		if vi, ok := tryAdd(vr.Introduced); ok {
-			m.Versions[i].Introduced = vi
-		}
-		if vi, ok := tryAdd(vr.Fixed); ok {
-			m.Versions[i].Fixed = vi
-		}
-	}
-}
-
-func sortModules(ms []*report.Module) {
-	sort.SliceStable(ms, func(i, j int) bool {
-		m1, m2 := ms[i], ms[j]
-		// Break ties by lowest affected version, assuming the version list is sorted.
-		if m1.Module == m2.Module {
-			vr1, vr2 := m1.Versions, m2.Versions
-			if len(vr1) == 0 {
-				return true
-			} else if len(vr2) == 0 {
-				return false
-			}
-
-			v1, v2 := first(vr1), first(vr2)
-			if v1 == v2 {
-				pkgs1, pkgs2 := m1.Packages, m2.Packages
-				if len(pkgs1) == 0 {
-					return true
-				} else if len(pkgs2) == 0 {
-					return false
-				}
-				return pkgs1[0].Package < pkgs2[0].Package
-			}
-
-			return version.Before(v1, v2)
-		}
-		return m1.Module < m2.Module
-	})
-}
-
-// merge merges all modules with the same module & package info
-// (but possibly different versions) into one.
-func merge(ms []*report.Module) []*report.Module {
-	type compMod struct {
-		path     string
-		packages string // sorted, comma separated list of package names
-	}
-
-	toCompMod := func(m *report.Module) compMod {
-		var packages []string
-		for _, p := range m.Packages {
-			packages = append(packages, p.Package)
-		}
-		return compMod{
-			path:     m.Module,
-			packages: strings.Join(packages, ","),
-		}
-	}
-
-	merge := func(m1, m2 *report.Module) *report.Module {
-		// only run if m1 and m2 are same except versions
-		// deletes vulnerable_at if set
-		return &report.Module{
-			Module:              m1.Module,
-			Versions:            append(m1.Versions, m2.Versions...),
-			UnsupportedVersions: append(m1.UnsupportedVersions, m2.UnsupportedVersions...),
-			Packages:            m1.Packages,
-		}
-	}
-
-	modules := make(map[compMod]*report.Module)
-	for _, m := range ms {
-		c := toCompMod(m)
-		mod, ok := modules[c]
-		if !ok {
-			modules[c] = m
-		} else {
-			modules[c] = merge(mod, m)
-		}
-	}
-
-	return maps.Values(modules)
-}
-
-func first(vrs []report.VersionRange) string {
-	for _, vr := range vrs {
-		for _, v := range []string{vr.Introduced, vr.Fixed} {
-			if v != "" {
-				return v
-			}
-		}
-	}
-	return ""
-}
-
 func convertVersions(rs []osvschema.Range) ([]report.VersionRange, []report.UnsupportedVersion) {
 	var vrs []report.VersionRange
 	var uvs []report.UnsupportedVersion
@@ -402,10 +106,6 @@
 	return vrs, uvs
 }
 
-var (
-	goAdvisory = regexp.MustCompile(`^https://pkg.go.dev/vuln/.*$`)
-)
-
 func convertRef(ref osvschema.Reference) *report.Reference {
 	return &report.Reference{
 		Type: osv.ReferenceType(ref.Type),
@@ -413,165 +113,6 @@
 	}
 }
 
-// fixRefs deletes some unneeded references, and attempts to fix reference types.
-// Modifies r.
-//
-// Deletes:
-//   - "package"-type references
-//   - Go advisory references (these are redundant for us)
-//   - all advisories except the "best" one (if applicable)
-//
-// Changes:
-//   - reference type to "advisory" for GHSA and CVE links.
-//   - reference type to "fix" for Github pull requests and commit links in one of
-//     the affected modules
-//   - reference type to "report" for Github issues in one of
-//     the affected modules
-func fixRefs(r *report.Report) {
-	re := newRE(r)
-
-	for _, ref := range r.References {
-		switch re.Type(ref.URL) {
-		case urlTypeAdvisory:
-			ref.Type = osv.ReferenceTypeAdvisory
-		case urlTypeIssue:
-			ref.Type = osv.ReferenceTypeReport
-		case urlTypeFix:
-			ref.Type = osv.ReferenceTypeFix
-		}
-	}
-
-	bestAdvisory, found := re.bestAdvisory(r.References)
-	isUnnecessaryAdvisory := func(ref *report.Reference) bool {
-		return found && ref.Type == osv.ReferenceTypeAdvisory && ref.URL != bestAdvisory
-	}
-
-	r.References = slices.DeleteFunc(r.References, func(ref *report.Reference) bool {
-		return ref.Type == osv.ReferenceTypePackage ||
-			goAdvisory.MatchString(ref.URL) ||
-			isUnnecessaryAdvisory(ref)
-	})
-
-	if len(r.References) == 0 {
-		r.References = nil
-	}
-}
-
-// bestAdvisory returns the URL of the "best" advisory in the references,
-// or ("", false) if none can be found.
-// Repository-level GHSAs are considered the best, followed by regular
-// GHSAs, followed by CVEs.
-// For now, if there are advisories mentioning two or more
-// aliases of the same type, we don't try to determine which is best.
-// (For example, if there are two advisories, referencing GHSA-1 and GHSA-2, we leave it
-// to the triager to pick the best one.)
-func (re *reportRE) bestAdvisory(refs []*report.Reference) (string, bool) {
-	bestAdvisory := ""
-	bestType := advisoryTypeUnknown
-	ghsas, cves := make(map[string]bool), make(map[string]bool)
-	for _, ref := range refs {
-		if ref.Type != osv.ReferenceTypeAdvisory {
-			continue
-		}
-		t, alias := re.AdvisoryType(ref.URL)
-		if t > bestType {
-			bestAdvisory = ref.URL
-			bestType = t
-		}
-
-		if ghsa.IsGHSA(alias) {
-			ghsas[alias] = true
-		} else if cveschema5.IsCVE(alias) {
-			cves[alias] = true
-		}
-	}
-
-	if len(ghsas) > 1 || len(cves) > 1 {
-		return "", false
-	}
-
-	return bestAdvisory, bestAdvisory != ""
-}
-
-type urlType int
-
-const (
-	urlTypeUnknown urlType = iota
-	urlTypeIssue
-	urlTypeFix
-	urlTypeAdvisory
-)
-
-func (re *reportRE) Type(url string) urlType {
-	switch {
-	case re.ghsa.MatchString(url):
-		return urlTypeAdvisory
-	case re.ghsaRepo.MatchString(url):
-		return urlTypeAdvisory
-	case re.cve.MatchString(url):
-		return urlTypeAdvisory
-	case re.issue.MatchString(url):
-		return urlTypeIssue
-	case re.fix.MatchString(url):
-		return urlTypeFix
-	}
-	return urlTypeUnknown
-}
-
-type advisoryType int
-
-// Advisory link types in ascending order of (likely) quality.
-// In general, repo-level GHSAs tend to be the best because
-// they are more likely to be directly created by a maintainer.
-const (
-	advisoryTypeUnknown advisoryType = iota
-	advisoryTypeCVE
-	advisoryTypeGHSA
-	advisoryTypeGHSARepo
-)
-
-func (re *reportRE) AdvisoryType(url string) (t advisoryType, alias string) {
-	if m := re.ghsa.FindStringSubmatch(url); len(m) == 2 {
-		return advisoryTypeGHSA, m[1]
-	}
-
-	if m := re.ghsaRepo.FindStringSubmatch(url); len(m) == 2 {
-		return advisoryTypeGHSARepo, m[1]
-	}
-
-	if m := re.cve.FindStringSubmatch(url); len(m) == 2 {
-		return advisoryTypeCVE, m[1]
-	}
-
-	return advisoryTypeUnknown, ""
-}
-
-type reportRE struct {
-	ghsa, ghsaRepo, cve, issue, fix *regexp.Regexp
-}
-
-func newRE(r *report.Report) *reportRE {
-	oneOfRE := func(s []string) string {
-		return `(` + strings.Join(s, "|") + `)`
-	}
-
-	// For now, this will not attempt to fix reference types for
-	// modules whose canonical names are different from their github path.
-	var modulePaths []string
-	for _, m := range r.Modules {
-		modulePaths = append(modulePaths, m.Module)
-	}
-	moduleRE := oneOfRE(modulePaths)
-
-	return &reportRE{
-		ghsa:     regexp.MustCompile(`^https://github.com/advisories/` + oneOfRE(r.GHSAs) + `$`),
-		ghsaRepo: regexp.MustCompile(`^https://github.com/.+/advisories/` + oneOfRE(r.GHSAs) + `$`),
-		cve:      regexp.MustCompile(`^https://nvd.nist.gov/vuln/detail/` + oneOfRE(r.CVEs) + `$`),
-		issue:    regexp.MustCompile(`https://` + moduleRE + `/issue(s?)/.*$`),
-		fix:      regexp.MustCompile(`https://` + moduleRE + `/(commit(s?)|pull)/.*$`),
-	}
-}
-
 func convertCredits(cs []osvschema.Credit) []string {
 	var credits []string
 	for _, c := range cs {
diff --git a/internal/genericosv/report_test.go b/internal/genericosv/report_test.go
index c4f82ab..4e410e4 100644
--- a/internal/genericosv/report_test.go
+++ b/internal/genericosv/report_test.go
@@ -12,8 +12,6 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	osvschema "github.com/google/osv-scanner/pkg/models"
-	"golang.org/x/vulndb/internal/osv"
 	"golang.org/x/vulndb/internal/proxy"
 	"golang.org/x/vulndb/internal/report"
 )
@@ -78,460 +76,3 @@
 		t.Fatal(err)
 	}
 }
-
-// TODO(https://go.dev/issues/61769): unskip test cases as we add features.
-// To update proxy responses:
-// go test ./internal/genericosv/... -proxy -run TestAffectedToModules
-func TestAffectedToModules(t *testing.T) {
-	for _, tc := range []struct {
-		name string
-		desc string
-		in   []osvschema.Affected
-		want []*report.Module
-		skip bool
-	}{
-		{
-			name: "ok",
-			desc: "module is already OK",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/influxdata/influxdb",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Introduced: "0.3.2",
-						},
-						{
-							Fixed: "1.7.6",
-						},
-					},
-				}},
-			}},
-			want: []*report.Module{{
-				Module: "github.com/influxdata/influxdb",
-				Versions: []report.VersionRange{
-					{
-						Introduced: "0.3.2",
-						Fixed:      "1.7.6",
-					}},
-				VulnerableAt: "1.7.5",
-			}},
-		},
-		{
-			name: "import_path",
-			desc: "find module from import path",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/influxdata/influxdb/services/httpd",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Introduced: "0.3.2",
-						},
-						{
-							Fixed: "1.7.6",
-						},
-					},
-				}},
-			}},
-			want: []*report.Module{{
-				Module: "github.com/influxdata/influxdb",
-				Versions: []report.VersionRange{
-					{
-						Introduced: "0.3.2",
-						Fixed:      "1.7.6",
-					},
-				},
-				Packages: []*report.Package{
-					{
-						Package: "github.com/influxdata/influxdb/services/httpd",
-					},
-				},
-				VulnerableAt: "1.7.5",
-			}},
-		},
-		{
-			name: "major_version",
-			desc: "correct major version of module path",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/nats-io/nats-server",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Introduced: "2.2.0",
-						},
-						{
-							Fixed: "2.8.3",
-						},
-					},
-				}},
-			}},
-			want: []*report.Module{{
-				Module: "github.com/nats-io/nats-server/v2",
-				Versions: []report.VersionRange{
-					{
-						Introduced: "2.2.0",
-						Fixed:      "2.8.3",
-					},
-				},
-				VulnerableAt: "2.8.2",
-			}},
-		},
-		{
-			name: "canonicalize",
-			desc: "canonicalize module path",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/golang/vulndb",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Fixed: "0.0.0-20230712151357-4fee11d0f8f9",
-						},
-					},
-				}},
-			}},
-			want: []*report.Module{{
-				Module: "golang.org/x/vulndb",
-				Versions: []report.VersionRange{
-					{
-						Fixed: "0.0.0-20230712151357-4fee11d0f8f9",
-					},
-				},
-			}},
-		},
-		{
-			name: "add_incompatible",
-			desc: "add +incompatible",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/docker/docker",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Fixed: "23.0.0",
-						},
-					},
-				}},
-			}},
-			want: []*report.Module{{
-				Module: "github.com/docker/docker",
-				Versions: []report.VersionRange{
-					{
-						Fixed: "23.0.0+incompatible",
-					},
-				},
-				VulnerableAt: "23.0.0-rc.4+incompatible",
-			}},
-		},
-		{
-			name: "merge_modules",
-			desc: "merge modules that are the same except for versions",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/hashicorp/go-getter/v2",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Introduced: "2.0.0",
-						},
-						{
-							Fixed: "2.0.2",
-						},
-					},
-				}},
-			},
-				{
-					Package: osvschema.Package{
-						Ecosystem: osvschema.EcosystemGo,
-						Name:      "github.com/hashicorp/go-getter/v2",
-					},
-					Ranges: []osvschema.Range{{
-						Type: osvschema.RangeEcosystem,
-						Events: []osvschema.Event{
-							{
-								Introduced: "2.1.0",
-							},
-							{
-								Fixed: "2.1.1",
-							},
-						},
-					}},
-				}},
-			want: []*report.Module{{
-				Module: "github.com/hashicorp/go-getter/v2",
-				Versions: []report.VersionRange{
-					{
-						Introduced: "2.0.0",
-						Fixed:      "2.0.2",
-					},
-					{
-						Introduced: "2.1.0",
-						Fixed:      "2.1.1",
-					},
-				},
-				VulnerableAt: "2.1.0",
-			}},
-		},
-		{
-			name: "remove_duplicates",
-			desc: "remove major version duplicates",
-			in: []osvschema.Affected{{
-				Package: osvschema.Package{
-					Ecosystem: osvschema.EcosystemGo,
-					Name:      "github.com/hashicorp/go-getter/v2",
-				},
-				Ranges: []osvschema.Range{{
-					Type: osvschema.RangeEcosystem,
-					Events: []osvschema.Event{
-						{
-							Introduced: "0",
-						},
-						{
-							Fixed: "2.1.0",
-						},
-					},
-				}},
-			},
-				{
-					Package: osvschema.Package{
-						Ecosystem: osvschema.EcosystemGo,
-						Name:      "github.com/hashicorp/go-getter",
-					},
-					Ranges: []osvschema.Range{{
-						Type: osvschema.RangeEcosystem,
-						Events: []osvschema.Event{
-							{
-								Introduced: "2.0.0",
-							},
-							{
-								Fixed: "2.1.0",
-							},
-						},
-					}},
-				}},
-			want: []*report.Module{{
-				Module: "github.com/hashicorp/go-getter/v2",
-				Versions: []report.VersionRange{
-					{
-						Introduced: "2.0.0",
-						Fixed:      "2.1.0",
-					},
-				},
-			}},
-			skip: true,
-		},
-	} {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-			if tc.skip {
-				t.Skip("skipping (not implemented yet)")
-			}
-
-			pc, err := proxy.NewTestClient(t, *realProxy)
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			got := affectedToModules(tc.in, pc)
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Errorf("%s: affectedToModules() mismatch (-want +got)\n%s", tc.desc, diff)
-			}
-		})
-
-	}
-}
-
-func TestFixRefs(t *testing.T) {
-	for _, tc := range []struct {
-		name     string
-		in, want []*report.Reference
-	}{
-		{
-			// GHSA references are converted to advisory type
-			name: "to_advisory_ghsa",
-			in: []*report.Reference{
-				{
-					URL:  "https://github.com/example/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/other/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
-					Type: osv.ReferenceTypeWeb,
-				},
-			},
-			want: []*report.Reference{
-				{
-					URL:  "https://github.com/example/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
-					Type: osv.ReferenceTypeAdvisory,
-				},
-				{
-					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
-					Type: osv.ReferenceTypeAdvisory,
-				},
-				{
-					URL:  "https://github.com/other/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
-					Type: osv.ReferenceTypeAdvisory, // different module OK, because GHSA matches
-				},
-			},
-		},
-		{
-			// CVE references are converted to advisory type
-			name: "to_advisory_cve",
-			in: []*report.Reference{
-				{
-					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-0001",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-2222",
-					Type: osv.ReferenceTypeWeb,
-				},
-			},
-			want: []*report.Reference{
-				{
-					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-0001",
-					Type: osv.ReferenceTypeAdvisory,
-				},
-				{
-					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-2222",
-					Type: osv.ReferenceTypeWeb, // different CVE, keep "web" type
-				},
-			},
-		},
-		{
-			// CVE references are removed if GHSA is present
-			name: "remove_cve",
-			in: []*report.Reference{
-				{
-					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-0001",
-					Type: osv.ReferenceTypeWeb,
-				},
-			},
-			want: []*report.Reference{
-				{
-					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
-					Type: osv.ReferenceTypeAdvisory,
-				},
-			},
-		},
-		{
-			name: "to_fix_or_report",
-			in: []*report.Reference{
-				{
-					URL:  "https://github.com/example/module/pull/123",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/example/module/commit/123",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/module/module/issues/123",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/example/module/issue/123",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/different/module/issue/123",
-					Type: osv.ReferenceTypeWeb,
-				},
-			},
-			want: []*report.Reference{
-				{
-					URL:  "https://github.com/example/module/pull/123",
-					Type: osv.ReferenceTypeFix,
-				},
-				{
-					URL:  "https://github.com/example/module/commit/123",
-					Type: osv.ReferenceTypeFix,
-				},
-				{
-					URL:  "https://github.com/module/module/issues/123",
-					Type: osv.ReferenceTypeReport,
-				},
-				{
-					URL:  "https://github.com/example/module/issue/123",
-					Type: osv.ReferenceTypeReport,
-				},
-				{
-					URL:  "https://github.com/different/module/issue/123",
-					Type: osv.ReferenceTypeWeb, // different module, keep web type
-				},
-			},
-		},
-		{
-			// package references and go advisory references are deleted
-			name: "delete",
-			in: []*report.Reference{
-				{
-					URL:  "https://pkg.go.dev/vuln/GO-0000-0000",
-					Type: osv.ReferenceTypeWeb,
-				},
-				{
-					URL:  "https://github.com/anything",
-					Type: osv.ReferenceTypePackage,
-				},
-				{
-					URL:  "https://example.com",
-					Type: osv.ReferenceTypePackage,
-				},
-			},
-			want: nil,
-		},
-	} {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-
-			r := &report.Report{
-				Modules: []*report.Module{
-					{
-						Module: "github.com/example/module",
-					},
-					{
-						Module: "github.com/module/module",
-					},
-				},
-				GHSAs:      []string{"GHSA-xxxx-yyyy-zzzz", "GHSA-gggg-hhhh-ffff"},
-				CVEs:       []string{"CVE-1999-0001", "CVE-1999-0002"},
-				References: tc.in,
-			}
-			fixRefs(r)
-			got := r.References
-			if diff := cmp.Diff(tc.want, got); diff != "" {
-				t.Errorf("fixRefs() mismatch (-want +got)\n%s", diff)
-			}
-		})
-	}
-}
diff --git a/internal/genericosv/testdata/proxy/TestAffectedToModules/add_incompatible.json b/internal/genericosv/testdata/proxy/TestAffectedToModules/add_incompatible.json
deleted file mode 100644
index 6b64701..0000000
--- a/internal/genericosv/testdata/proxy/TestAffectedToModules/add_incompatible.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-	"github.com/docker/docker/@v/list": {
-		"body": "v1.9.0\nv1.6.0-rc3\nv1.3.0\nv1.12.0-rc3\nv1.8.2-rc1\nv20.10.11+incompatible\nv24.0.3+incompatible\nv1.7.0\nv23.0.0-rc.3+incompatible\nv1.7.0-rc5\nv0.1.4\nv1.12.2-rc3\nv1.11.0-rc3\nv1.13.0-rc2\nv23.0.3+incompatible\nv20.10.21+incompatible\nv17.11.0-ce-rc3+incompatible\nv17.10.0-ce+incompatible\nv1.12.0-rc2\nv0.7.5\nv0.2.2\nv0.7.0-rc1\nv1.6.0-rc7\nv0.7.0\nv1.13.1-rc1\nv1.10.0-rc3\nv0.1.3\nv20.10.6+incompatible\nv0.8.1\nv1.13.1\nv20.10.1+incompatible\nv0.7.3\nv17.12.1-ce+incompatible\nv1.8.0\nv20.10.0+incompatible\nv1.12.0-rc4\nv0.6.6\nv20.10.4+incompatible\nv1.9.0-rc2\nv1.12.2-rc2\nv0.4.5\nv0.3.0\nv0.4.6\nv1.6.0\nv0.5.2\nv0.6.1\nv20.10.23+incompatible\nv0.3.4\nv1.11.2\nv1.0.0\nv0.7.2\nv0.8.0\nv23.0.0-rc.2+incompatible\nv1.8.3\nv24.0.1+incompatible\nv1.7.0-rc1\nv1.12.0\nv0.4.3\nv0.6.0\nv20.10.10-rc1+incompatible\nv23.0.2+incompatible\nv1.3.3\nv24.0.2+incompatible\nv1.13.0-rc6\nv1.11.2-rc1\nv0.10.0\nv0.7.4\nv1.12.4-rc1\nv0.5.3\nv1.8.0-rc1\nv0.7.0-rc2\nv1.5.0-rc4\nv1.6.0-rc2\nv0.6.2\nv24.0.0+incompatible\nv23.0.0+incompatible\nv1.7.0-rc3\nv20.10.15+incompatible\nv17.10.0-ce-rc2+incompatible\nv20.10.17+incompatible\nv0.5.0\nv24.0.0-rc.2+incompatible\nv1.13.1-rc2\nv1.8.2\nv20.10.16+incompatible\nv1.9.0-rc1\nv20.10.2+incompatible\nv20.10.22+incompatible\nv1.1.1\nv20.10.20+incompatible\nv1.12.6\nv20.10.0-rc2+incompatible\nv1.12.1\nv1.5.0-rc2\nv1.3.1\nv0.1.7\nv17.12.0-ce-rc1+incompatible\nv1.5.0\nv17.11.0-ce+incompatible\nv23.0.6+incompatible\nv1.11.0-rc5\nv1.11.0-rc4\nv0.12.0\nv0.1.6\nv17.11.0-ce-rc1+incompatible\nv1.7.1\nv1.4.1\nv20.10.9+incompatible\nv1.7.1-rc1\nv17.12.0-ce-rc3+incompatible\nv1.6.0-rc6\nv1.5.0-rc1\nv0.7.0-rc5\nv1.1.2\nv20.10.18+incompatible\nv1.9.1\nv17.11.0-ce-rc2+incompatible\nv1.12.3\nv1.10.0\nv17.12.1-ce-rc2+incompatible\nv1.8.0-rc3\nv0.1.2\nv0.3.3\nv24.0.0-rc.4+incompatible\nv0.4.4\nv20.10.14+incompatible\nv0.7.0-rc4\nv0.9.0\nv0.6.5\nv0.4.0\nv0.1.8\nv0.4.1\nv20.10.8+incompatible\nv1.11.0-rc2\nv1.7.1-rc3\nv23.0.0-rc.4+incompatible\nv20.10.0-rc1+incompatible\nv0.5.1\nv0.7.0-rc7\nv24.0.0-beta.2+incompatible\nv1.9.1-rc1\nv1.11.0\nv23.0.1+incompatible\nv24.0.6+incompatible\nv23.0.5+incompatible\nv20.10.3+incompatible\nv1.10.1-rc1\nv1.12.5\nv1.6.0-rc4\nv1.10.2\nv1.13.0-rc5\nv24.0.0-rc.1+incompatible\nv24.0.5+incompatible\nv1.3.2\nv17.12.0-ce-rc2+incompatible\nv0.3.1\nv0.2.1\nv0.11.0\nv1.10.2-rc1\nv1.7.0-rc2\nv0.1.0\nv17.12.0-ce-rc4+incompatible\nv1.11.1-rc1\nv20.10.10+incompatible\nv1.4.0\nv0.7.1\nv0.1.5\nv1.13.0-rc1\nv0.9.1\nv1.2.0\nv1.13.0-rc7\nv0.6.3\nv0.1.1\nv24.0.0-beta.1+incompatible\nv1.12.1-rc2\nv20.10.0-beta1+incompatible\nv0.4.7\nv1.12.2\nv1.10.3-rc2\nv1.9.0-rc4\nv1.10.0-rc2\nv20.10.5+incompatible\nv20.10.13+incompatible\nv1.6.1\nv1.13.0-rc3\nv1.12.4\nv1.10.1\nv0.7.0-rc6\nv1.13.0-rc4\nv0.7.6\nv1.12.5-rc1\nv24.0.4+incompatible\nv1.12.3-rc1\nv0.11.1\nv1.7.0-rc4\nv1.12.1-rc1\nv1.7.1-rc2\nv1.10.3-rc1\nv0.6.7\nv1.13.0\nv1.6.0-rc1\nv1.10.0-rc4\nv1.8.1\nv1.10.3\nv1.12.0-rc5\nv1.6.2\nv1.9.0-rc3\nv17.10.0-ce-rc1+incompatible\nv1.0.1\nv1.1.0\nv17.11.0-ce-rc4+incompatible\nv20.10.24+incompatible\nv0.6.4\nv1.11.1\nv1.11.0-rc1\nv1.12.0-rc1\nv1.5.0-rc3\nv0.2.0\nv1.8.0-rc2\nv1.10.0-rc1\nv0.4.2\nv23.0.0-beta.1+incompatible\nv20.10.25+incompatible\nv17.12.1-ce-rc1+incompatible\nv24.0.0-rc.3+incompatible\nv20.10.7+incompatible\nv0.3.2\nv23.0.0-rc.1+incompatible\nv0.4.8\nv1.9.0-rc5\nv0.7.0-rc3\nv20.10.12+incompatible\nv17.12.0-ce+incompatible\nv1.6.0-rc5\nv20.10.19+incompatible\nv23.0.4+incompatible\nv1.12.2-rc1\n",
-		"status_code": 200
-	},
-	"github.com/docker/docker/@v/v23.0.0+incompatible.mod": {
-		"body": "module github.com/docker/docker\n",
-		"status_code": 200
-	}
-}
\ No newline at end of file
diff --git a/internal/genericosv/testdata/proxy/TestAffectedToModules/import_path.json b/internal/genericosv/testdata/proxy/TestAffectedToModules/import_path.json
deleted file mode 100644
index 8b1da09..0000000
--- a/internal/genericosv/testdata/proxy/TestAffectedToModules/import_path.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-	"github.com/influxdata/influxdb/@v/list": {
-		"body": "v1.9.0\nv0.8.0-rc.2\nv1.3.0\nv1.0.2\nv0.5.8\nv0.9.0-rc22\nv0.9.0-rc21\nv0.9.5-rc3\nv1.7.0\nv1.8.8\nv0.9.0-rc23\nv1.0.0-beta3\nv0.10.0-rc2\nv0.9.0-rc3\nv0.5.5\nv0.5.0-rc.4\nv1.2.3\nv0.9.3\nv0.7.0\nv1.7.9\nv1.1.0-rc2\nv0.9.0-rc33\nv0.8.1\nv0.9.0-rc24\nv1.9.6\nv0.9.0-rc18\nv0.9.2\nv0.7.3\nv1.9.8\nv1.5.1\nv1.8.0\nv1.2.1-rc4\nv1.6.4\nv0.9.0-rc13\nv0.9.0-rc29\nv0.9.0-rc14\nv1.6.5\nv0.5.0-rc.3\nv0.8.8\nv0.9.0-rc10\nv0.3.0\nv1.7.9-dev1\nv1.7.8\nv0.11.0-rc1\nv1.6.0\nv0.5.2\nv1.4.2\nv0.6.1\nv1.11.2\nv0.9.0-rc19\nv1.9.7\nv1.0.0\nv0.7.2\nv1.8.10\nv0.8.0\nv0.9.0-rc4\nv1.8.3\nv0.9.0-rc12\nv0.9.0-rc27\nv0.5.11\nv1.4.3\nv0.8.3\nv0.4.3\nv1.8.5\nv0.6.0\nv0.9.0-rc31\nv0.0.1\nv0.9.1-rc2\nv0.5.0-rc.6\nv1.3.3\nv0.10.0-beta2\nv0.5.6\nv0.10.0-beta1\nv0.10.0\nv1.8.7\nv0.5.3\nv1.3.6\nv0.10.2\nv1.1.0-rc1\nv0.6.2\nv0.5.0-rc.2\nv1.8.9\nv0.5.0\nv0.9.0-rc9\nv1.8.2\nv0.9.0-rc6\nv1.5.5\nv0.9.0-rc7\nv1.1.1\nv0.5.0-rc.5\nv0.8.0-rc.4\nv0.9.6-rc1\nv0.9.0-rc30\nv0.9.5\nv1.8.6\nv1.9.12\nv0.8.0-rc.5\nv0.10.3\nv1.3.1\nv1.0.0-beta1\nv1.5.0\nv1.2.3-gershontest\nv1.3.4\nv0.0.5\nv1.7.2\nv0.12.0\nv0.8.7\nv1.7.1\nv1.4.1\nv0.12.2\nv0.9.0-rc20\nv1.1.3\nv1.7.11\nv1.1.2\nv1.6.1-exp-2-tr\nv1.9.1\nv1.6.3\nv0.8.5\nv1.10.0\nv0.9.0-rc16\nv1.7.3\nv0.5.0-rc.1\nv0.9.2-rc1\nv0.4.4\nv0.9.0\nv0.6.5\nv0.4.0\nv1.9.10\nv0.4.1\nv0.9.0-rc28\nv0.5.4\nv0.9.0-rc25\nv0.0.3\nv1.2.0-rc1\nv1.2.1-rc2\nv0.5.1\nv1.7.5\nv1.11.0\nv0.8.4\nv0.0.4\nv1.5.2\nv0.9.4-rc1\nv1.10.2\nv1.9.4\nv1.0.0-rc1\nv1.2.4\nv1.3.2\nv1.2.1\nv1.5.4\nv0.10.1\nv1.9.3\nv1.2.0-rc2\nv0.3.1\nv1.2.1-rc3\nv0.9.6-rc2\nv0.11.0\nv1.1.5\nv0.5.7\nv0.1.0\nv0.9.3-rc1\nv1.4.0\nv0.7.1\nv0.9.5-rc1\nv0.0.2\nv1.1.4\nv0.9.1\nv1.2.0\nv0.6.3\nv0.9.6\nv1.6.1-exp-tr\nv1.10.4\nv1.9.9\nv1.2.1-rc1\nv1.3.9\nv1.9.11\nv1.2.1-rc5\nv1.2.2\nv0.0.8\nv0.9.0-rc11\nv0.0.9\nv0.10.0-rc1\nv0.9.3-rc2\nv1.6.1\nv0.9.0-rc17\nv1.7.7\nv1.10.1\nv0.13.0-rc2\nv1.5.3\nv0.8.2\nv0.11.1\nv0.9.0-rc32\nv0.5.10\nv1.3.5\nv0.13.0\nv1.9.2\nv0.5.9\nv1.6.6\nv0.8.0-rc.1\nv1.7.10\nv1.3.8\nv0.8.6\nv0.9.0-rc5\nv0.9.0-rc8\nv1.9.5\nv0.13.0-rc1\nv1.8.1\nv1.10.3\nv1.6.2\nv1.7.4\nv1.0.1\nv0.9.0-rc2\nv0.9.3-rc3\nv1.1.0\nv1.0.0-rc2\nv0.6.4\nv1.11.1\nv0.0.6\nv0.0.7\nv1.8.4\nv0.2.0\nv1.3.7\nv0.9.5-rc2\nv1.0.0-beta2\nv0.8.0-rc.3\nv0.4.2\nv0.9.0-rc15\nv0.8.9\nv0.8.4-rc.1\nv0.9.1-rc1\nv0.3.2\nv0.12.1\nv0.9.4\nv1.7.6\nv0.5.12\nv0.9.0-rc26\n",
-		"status_code": 200
-	},
-	"github.com/influxdata/influxdb/@v/v0.3.2.mod": {
-		"body": "module github.com/influxdata/influxdb\n",
-		"status_code": 200
-	},
-	"github.com/influxdata/influxdb/@v/v1.7.6.mod": {
-		"body": "module github.com/influxdata/influxdb\n",
-		"status_code": 200
-	},
-	"github.com/influxdata/influxdb/services/@v/list": {
-		"status_code": 404
-	},
-	"github.com/influxdata/influxdb/services/httpd/@v/list": {
-		"status_code": 404
-	}
-}
\ No newline at end of file
diff --git a/internal/genericosv/testdata/proxy/TestAffectedToModules/ok.json b/internal/genericosv/testdata/proxy/TestAffectedToModules/ok.json
deleted file mode 100644
index 657982d..0000000
--- a/internal/genericosv/testdata/proxy/TestAffectedToModules/ok.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-	"github.com/influxdata/influxdb/@v/list": {
-		"body": "v1.9.0\nv0.8.0-rc.2\nv1.3.0\nv1.0.2\nv0.5.8\nv0.9.0-rc22\nv0.9.0-rc21\nv0.9.5-rc3\nv1.7.0\nv1.8.8\nv0.9.0-rc23\nv1.0.0-beta3\nv0.10.0-rc2\nv0.9.0-rc3\nv0.5.5\nv0.5.0-rc.4\nv1.2.3\nv0.9.3\nv0.7.0\nv1.7.9\nv1.1.0-rc2\nv0.9.0-rc33\nv0.8.1\nv0.9.0-rc24\nv1.9.6\nv0.9.0-rc18\nv0.9.2\nv0.7.3\nv1.9.8\nv1.5.1\nv1.8.0\nv1.2.1-rc4\nv1.6.4\nv0.9.0-rc13\nv0.9.0-rc29\nv0.9.0-rc14\nv1.6.5\nv0.5.0-rc.3\nv0.8.8\nv0.9.0-rc10\nv0.3.0\nv1.7.9-dev1\nv1.7.8\nv0.11.0-rc1\nv1.6.0\nv0.5.2\nv1.4.2\nv0.6.1\nv1.11.2\nv0.9.0-rc19\nv1.9.7\nv1.0.0\nv0.7.2\nv1.8.10\nv0.8.0\nv0.9.0-rc4\nv1.8.3\nv0.9.0-rc12\nv0.9.0-rc27\nv0.5.11\nv1.4.3\nv0.8.3\nv0.4.3\nv1.8.5\nv0.6.0\nv0.9.0-rc31\nv0.0.1\nv0.9.1-rc2\nv0.5.0-rc.6\nv1.3.3\nv0.10.0-beta2\nv0.5.6\nv0.10.0-beta1\nv0.10.0\nv1.8.7\nv0.5.3\nv1.3.6\nv0.10.2\nv1.1.0-rc1\nv0.6.2\nv0.5.0-rc.2\nv1.8.9\nv0.5.0\nv0.9.0-rc9\nv1.8.2\nv0.9.0-rc6\nv1.5.5\nv0.9.0-rc7\nv1.1.1\nv0.5.0-rc.5\nv0.8.0-rc.4\nv0.9.6-rc1\nv0.9.0-rc30\nv0.9.5\nv1.8.6\nv1.9.12\nv0.8.0-rc.5\nv0.10.3\nv1.3.1\nv1.0.0-beta1\nv1.5.0\nv1.2.3-gershontest\nv1.3.4\nv0.0.5\nv1.7.2\nv0.12.0\nv0.8.7\nv1.7.1\nv1.4.1\nv0.12.2\nv0.9.0-rc20\nv1.1.3\nv1.7.11\nv1.1.2\nv1.6.1-exp-2-tr\nv1.9.1\nv1.6.3\nv0.8.5\nv1.10.0\nv0.9.0-rc16\nv1.7.3\nv0.5.0-rc.1\nv0.9.2-rc1\nv0.4.4\nv0.9.0\nv0.6.5\nv0.4.0\nv1.9.10\nv0.4.1\nv0.9.0-rc28\nv0.5.4\nv0.9.0-rc25\nv0.0.3\nv1.2.0-rc1\nv1.2.1-rc2\nv0.5.1\nv1.7.5\nv1.11.0\nv0.8.4\nv0.0.4\nv1.5.2\nv0.9.4-rc1\nv1.10.2\nv1.9.4\nv1.0.0-rc1\nv1.2.4\nv1.3.2\nv1.2.1\nv1.5.4\nv0.10.1\nv1.9.3\nv1.2.0-rc2\nv0.3.1\nv1.2.1-rc3\nv0.9.6-rc2\nv0.11.0\nv1.1.5\nv0.5.7\nv0.1.0\nv0.9.3-rc1\nv1.4.0\nv0.7.1\nv0.9.5-rc1\nv0.0.2\nv1.1.4\nv0.9.1\nv1.2.0\nv0.6.3\nv0.9.6\nv1.6.1-exp-tr\nv1.10.4\nv1.9.9\nv1.2.1-rc1\nv1.3.9\nv1.9.11\nv1.2.1-rc5\nv1.2.2\nv0.0.8\nv0.9.0-rc11\nv0.0.9\nv0.10.0-rc1\nv0.9.3-rc2\nv1.6.1\nv0.9.0-rc17\nv1.7.7\nv1.10.1\nv0.13.0-rc2\nv1.5.3\nv0.8.2\nv0.11.1\nv0.9.0-rc32\nv0.5.10\nv1.3.5\nv0.13.0\nv1.9.2\nv0.5.9\nv1.6.6\nv0.8.0-rc.1\nv1.7.10\nv1.3.8\nv0.8.6\nv0.9.0-rc5\nv0.9.0-rc8\nv1.9.5\nv0.13.0-rc1\nv1.8.1\nv1.10.3\nv1.6.2\nv1.7.4\nv1.0.1\nv0.9.0-rc2\nv0.9.3-rc3\nv1.1.0\nv1.0.0-rc2\nv0.6.4\nv1.11.1\nv0.0.6\nv0.0.7\nv1.8.4\nv0.2.0\nv1.3.7\nv0.9.5-rc2\nv1.0.0-beta2\nv0.8.0-rc.3\nv0.4.2\nv0.9.0-rc15\nv0.8.9\nv0.8.4-rc.1\nv0.9.1-rc1\nv0.3.2\nv0.12.1\nv0.9.4\nv1.7.6\nv0.5.12\nv0.9.0-rc26\n",
-		"status_code": 200
-	},
-	"github.com/influxdata/influxdb/@v/v0.3.2.mod": {
-		"body": "module github.com/influxdata/influxdb\n",
-		"status_code": 200
-	},
-	"github.com/influxdata/influxdb/@v/v1.7.6.mod": {
-		"body": "module github.com/influxdata/influxdb\n",
-		"status_code": 200
-	}
-}
\ No newline at end of file
diff --git a/internal/report/cve.go b/internal/report/cve.go
index 6cfdebc..ed7ba50 100644
--- a/internal/report/cve.go
+++ b/internal/report/cve.go
@@ -11,7 +11,6 @@
 
 	"golang.org/x/vulndb/internal/cveschema"
 	"golang.org/x/vulndb/internal/cveschema5"
-	"golang.org/x/vulndb/internal/proxy"
 	"golang.org/x/vulndb/internal/stdlib"
 	"golang.org/x/vulndb/internal/version"
 )
@@ -35,7 +34,7 @@
 }
 
 // cveToReport creates a Report struct from a given CVE and modulePath.
-func cveToReport(c *cveschema.CVE, id, modulePath string, pc *proxy.Client) *Report {
+func cveToReport(c *cveschema.CVE, modulePath string) *Report {
 	var description Description
 	for _, d := range c.Description.Data {
 		description += Description(d.Value + "\n")
@@ -66,7 +65,6 @@
 		pkgPath = modulePath
 	}
 	r := &Report{
-		ID: id,
 		Modules: []*Module{{
 			Module: modulePath,
 			Packages: []*Package{{
@@ -76,13 +74,8 @@
 		Description: description,
 		Credits:     credits,
 		References:  refs,
-		SourceMeta: &SourceMeta{
-			ID: c.Metadata.ID,
-		},
 	}
 	r.addCVE(c.Metadata.ID, getCWE(c), isGoCNA(c))
-
-	r.Fix(pc)
 	return r
 }
 
@@ -108,7 +101,7 @@
 	r.CVEs = append(r.CVEs, cveID)
 }
 
-func cve5ToReport(c *cveschema5.CVERecord, id, modulePath string, pc *proxy.Client) *Report {
+func cve5ToReport(c *cveschema5.CVERecord, modulePath string) *Report {
 	cna := c.Containers.CNAContainer
 
 	var description Description
@@ -129,19 +122,14 @@
 	}
 
 	r := &Report{
-		ID:          id,
 		Modules:     affectedToModules(cna.Affected, modulePath),
 		Summary:     Summary(cna.Title),
 		Description: description,
 		Credits:     credits,
 		References:  refs,
-		SourceMeta: &SourceMeta{
-			ID: c.Metadata.ID,
-		},
 	}
 
 	r.addCVE(c.Metadata.ID, getCWE5(&cna), isGoCNA5(&cna))
-	r.Fix(pc)
 	return r
 }
 
diff --git a/internal/report/fix.go b/internal/report/fix.go
index 389ee65..fb36aa0 100644
--- a/internal/report/fix.go
+++ b/internal/report/fix.go
@@ -11,7 +11,11 @@
 	"sort"
 	"strings"
 
+	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
+	"golang.org/x/mod/module"
+	"golang.org/x/vulndb/internal/cveschema5"
+	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/osv"
 	"golang.org/x/vulndb/internal/proxy"
 	"golang.org/x/vulndb/internal/version"
@@ -19,9 +23,7 @@
 
 func (r *Report) Fix(pc *proxy.Client) {
 	expandGitCommits(r)
-	for _, m := range r.Modules {
-		m.FixVersions(pc)
-	}
+	r.FixModules(pc)
 	r.FixText()
 	r.FixReferences()
 }
@@ -59,25 +61,6 @@
 	r.Summary = Summary(summary)
 }
 
-func (r *Report) FixReferences() {
-	for _, ref := range r.References {
-		ref.URL = fixURL(ref.URL)
-	}
-	if r.missingAdvisory(r.countAdvisories()) {
-		r.addAdvisory()
-	}
-}
-
-func (r *Report) addAdvisory() {
-	// For now, only add an advisory if there is a CVE.
-	if len(r.CVEs) > 0 {
-		r.References = append(r.References, &Reference{
-			Type: osv.ReferenceTypeAdvisory,
-			URL:  fmt.Sprintf("%s%s", NISTPrefix, r.CVEs[0]),
-		})
-	}
-}
-
 // FixVersions replaces each version with its canonical form (if possible),
 // sorts version ranges, and collects version ranges into a compact form.
 func (m *Module) FixVersions(pc *proxy.Client) {
@@ -272,3 +255,468 @@
 	}
 	return u
 }
+
+func (r *Report) FixModules(pc *proxy.Client) {
+	for _, m := range r.Modules {
+		extractImportPath(m, pc)
+		if ok := fixMajorVersion(m, pc); !ok {
+			addIncompatible(m, pc)
+		}
+		canonicalize(m, pc)
+	}
+
+	r.Modules = merge(r.Modules)
+
+	// Fix the versions *after* the modules have been merged.
+	for _, m := range r.Modules {
+		m.FixVersions(pc)
+	}
+
+	sortModules(r.Modules)
+}
+
+// extractImportPath checks if the module m's "module" path is actually
+// an import path. If so, it adds the import path to the packages list
+// and fixes the module path. Modifies m.
+//
+// Does nothing if the module path is already correct, or isn't recognized
+// by the proxy at all.
+func extractImportPath(m *Module, pc *proxy.Client) {
+	path := m.Module
+	modulePath, err := pc.FindModule(m.Module)
+	if err != nil || // path doesn't contain a module, needs human review
+		path == modulePath { // path is already a module, no action needed
+		return
+	}
+	m.Module = modulePath
+	m.Packages = append(m.Packages, &Package{Package: path})
+}
+
+// fixMajorVersion corrects the major version prefix of the module
+// path if possible.
+// Returns true if the major version was already correct or could be
+// fixed.
+// For now, it gives up if it encounters various problems and
+// special cases (see comments inline).
+func fixMajorVersion(m *Module, pc *proxy.Client) (ok bool) {
+	if strings.HasPrefix(m.Module, "gopkg.in/") {
+		return false // don't attempt to fix gopkg.in modules
+	}
+	// If there is no "introduced" version, don't attempt to fix
+	// major version.
+	// Example: example.com/module is fixed at 2.2.2. This likely means
+	// that example.com/module is vulnerable at all versions and
+	// example.com/module/v2 is vulnerable up to 2.2.2.
+	// Changing example.com/module to example.com/module/v2 would lose
+	// information.
+	hasIntroduced := func(m *Module) bool {
+		for _, vr := range m.Versions {
+			if vr.Introduced != "" {
+				return true
+			}
+		}
+		return false
+	}
+	if !hasIntroduced(m) {
+		return false
+	}
+	wantMajor, ok := commonMajor(m.Versions)
+	if !ok { // inconsistent major version, don't attempt to fix
+		return false
+	}
+	prefix, major, ok := module.SplitPathVersion(m.Module)
+	if !ok { // couldn't parse module path, don't attempt to fix
+		return false
+	}
+	if major == wantMajor {
+		return true // nothing to do
+	}
+	fixed := prefix + wantMajor
+	if !pc.ModuleExists(fixed) {
+		return false // attempted fixed module doesn't exist, give up
+	}
+	m.Module = fixed
+	return true
+}
+
+const (
+	v0   = "v0"
+	v1   = "v1"
+	v0v1 = "v0 or v1"
+)
+
+func major(v string) string {
+	m := version.Major(v)
+	if m == v0 || m == v1 {
+		return v0v1
+	}
+	return m
+}
+
+// commonMajor returns the major version path suffix (e.g. "/v2") common
+// to all versions in the version range, or ("", false) if not all versions
+// have the same major version.
+// Returns ("", true) if the major version is 0 or 1.
+func commonMajor(vs []VersionRange) (_ string, ok bool) {
+	maj := major(first(vs))
+	for _, vr := range vs {
+		for _, v := range []string{vr.Introduced, vr.Fixed} {
+			if v == "" {
+				continue
+			}
+			current := major(v)
+			if current != maj {
+				return "", false
+			}
+		}
+	}
+	if maj == v0v1 {
+		return "", true
+	}
+	return "/" + maj, true
+}
+
+// canonicalize attempts to canonicalize the module path,
+// and updates the module path and packages list if successful.
+// Modifies m.
+//
+// Does nothing if the module path is already canonical, or isn't recognized
+// by the proxy at all.
+func canonicalize(m *Module, pc *proxy.Client) {
+	if len(m.Versions) == 0 {
+		return // no versions, don't attempt to fix
+	}
+
+	canonical, err := commonCanonical(m, pc)
+	if err != nil {
+		return // no consistent canonical version found, don't attempt to fix
+	}
+
+	original := m.Module
+	m.Module = canonical
+
+	// Fix any package paths.
+	for _, p := range m.Packages {
+		if strings.HasPrefix(p.Package, original) {
+			p.Package = canonical + strings.TrimPrefix(p.Package, original)
+		}
+	}
+}
+
+func commonCanonical(m *Module, pc *proxy.Client) (string, error) {
+	canonical, err := pc.CanonicalModulePath(m.Module, first(m.Versions))
+	if err != nil {
+		return "", err
+	}
+
+	for _, vr := range m.Versions {
+		for _, v := range []string{vr.Introduced, vr.Fixed} {
+			if v == "" {
+				continue
+			}
+			current, err := pc.CanonicalModulePath(m.Module, v)
+			if err != nil {
+				return "", err
+			}
+			if current != canonical {
+				return "", fmt.Errorf("inconsistent canonical module paths: %s and %s", canonical, current)
+			}
+		}
+	}
+	return canonical, nil
+}
+
+// addIncompatible adds "+incompatible" to all versions where module@version
+// does not exist but module@version+incompatible does exist.
+// TODO(https://go.dev/issue/61769): Consider making this work for
+// non-canonical versions too (example: GHSA-w4xh-w33p-4v29).
+func addIncompatible(m *Module, pc *proxy.Client) {
+	tryAdd := func(v string) (string, bool) {
+		if v == "" {
+			return "", false
+		}
+		if major(v) == v0v1 {
+			return "", false // +incompatible does not apply for major versions < 2
+		}
+		if pc.ModuleExistsAtTaggedVersion(m.Module, v) {
+			return "", false // module@version is already OK
+		}
+		if vi := v + "+incompatible"; pc.ModuleExistsAtTaggedVersion(m.Module, vi) {
+			return vi, true
+		}
+		return "", false // module@version+incompatible doesn't exist
+	}
+	for i, vr := range m.Versions {
+		if vi, ok := tryAdd(vr.Introduced); ok {
+			m.Versions[i].Introduced = vi
+		}
+		if vi, ok := tryAdd(vr.Fixed); ok {
+			m.Versions[i].Fixed = vi
+		}
+	}
+}
+
+func sortModules(ms []*Module) {
+	sort.SliceStable(ms, func(i, j int) bool {
+		m1, m2 := ms[i], ms[j]
+		// Break ties by lowest affected version, assuming the version list is sorted.
+		if m1.Module == m2.Module {
+			vr1, vr2 := m1.Versions, m2.Versions
+			if len(vr1) == 0 {
+				return true
+			} else if len(vr2) == 0 {
+				return false
+			}
+
+			v1, v2 := first(vr1), first(vr2)
+			if v1 == v2 {
+				pkgs1, pkgs2 := m1.Packages, m2.Packages
+				if len(pkgs1) == 0 {
+					return true
+				} else if len(pkgs2) == 0 {
+					return false
+				}
+				return pkgs1[0].Package < pkgs2[0].Package
+			}
+
+			return version.Before(v1, v2)
+		}
+		return m1.Module < m2.Module
+	})
+}
+
+// merge merges all modules with the same module & package info
+// (but possibly different versions) into one.
+func merge(ms []*Module) []*Module {
+	type compMod struct {
+		path     string
+		packages string // sorted, comma separated list of package names
+	}
+
+	toCompMod := func(m *Module) compMod {
+		var packages []string
+		for _, p := range m.Packages {
+			packages = append(packages, p.Package)
+		}
+		return compMod{
+			path:     m.Module,
+			packages: strings.Join(packages, ","),
+		}
+	}
+
+	merge := func(m1, m2 *Module) *Module {
+		// only run if m1 and m2 are same except versions
+		// deletes vulnerable_at if set
+		return &Module{
+			Module:              m1.Module,
+			Versions:            append(m1.Versions, m2.Versions...),
+			UnsupportedVersions: append(m1.UnsupportedVersions, m2.UnsupportedVersions...),
+			Packages:            m1.Packages,
+		}
+	}
+
+	modules := make(map[compMod]*Module)
+	for _, m := range ms {
+		c := toCompMod(m)
+		mod, ok := modules[c]
+		if !ok {
+			modules[c] = m
+		} else {
+			modules[c] = merge(mod, m)
+		}
+	}
+
+	return maps.Values(modules)
+}
+
+func first(vrs []VersionRange) string {
+	for _, vr := range vrs {
+		for _, v := range []string{vr.Introduced, vr.Fixed} {
+			if v != "" {
+				return v
+			}
+		}
+	}
+	return ""
+}
+
+var (
+	goAdvisory = regexp.MustCompile(`^https://pkg.go.dev/vuln/.*$`)
+)
+
+// FixReferences deletes some unneeded references, and attempts to fix reference types.
+// Modifies r.
+//
+// Deletes:
+//   - "package"-type references
+//   - Go advisory references (these are redundant for us)
+//   - all advisories except the "best" one (if applicable)
+//
+// Changes:
+//   - reference type to "advisory" for GHSA and CVE links.
+//   - reference type to "fix" for Github pull requests and commit links in one of
+//     the affected modules
+//   - reference type to "report" for Github issues in one of
+//     the affected modules
+func (r *Report) FixReferences() {
+	for _, ref := range r.References {
+		ref.URL = fixURL(ref.URL)
+	}
+
+	re := newRE(r)
+
+	for _, ref := range r.References {
+		switch re.Type(ref.URL) {
+		case urlTypeAdvisory:
+			ref.Type = osv.ReferenceTypeAdvisory
+		case urlTypeIssue:
+			ref.Type = osv.ReferenceTypeReport
+		case urlTypeFix:
+			ref.Type = osv.ReferenceTypeFix
+		}
+	}
+
+	bestAdvisory, found := re.bestAdvisory(r.References)
+	isUnnecessaryAdvisory := func(ref *Reference) bool {
+		return found && ref.Type == osv.ReferenceTypeAdvisory && ref.URL != bestAdvisory
+	}
+
+	r.References = slices.DeleteFunc(r.References, func(ref *Reference) bool {
+		return ref.Type == osv.ReferenceTypePackage ||
+			goAdvisory.MatchString(ref.URL) ||
+			isUnnecessaryAdvisory(ref)
+	})
+
+	if r.missingAdvisory(r.countAdvisories()) {
+		r.addAdvisory()
+	}
+
+	if len(r.References) == 0 {
+		r.References = nil
+	}
+}
+
+func (r *Report) addAdvisory() {
+	// For now, only add an advisory if there is a CVE.
+	if len(r.CVEs) > 0 {
+		r.References = append(r.References, &Reference{
+			Type: osv.ReferenceTypeAdvisory,
+			URL:  fmt.Sprintf("%s%s", NISTPrefix, r.CVEs[0]),
+		})
+	}
+}
+
+// bestAdvisory returns the URL of the "best" advisory in the references,
+// or ("", false) if none can be found.
+// Repository-level GHSAs are considered the best, followed by regular
+// GHSAs, followed by CVEs.
+// For now, if there are advisories mentioning two or more
+// aliases of the same type, we don't try to determine which is best.
+// (For example, if there are two advisories, referencing GHSA-1 and GHSA-2, we leave it
+// to the triager to pick the best one.)
+func (re *reportRE) bestAdvisory(refs []*Reference) (string, bool) {
+	bestAdvisory := ""
+	bestType := advisoryTypeUnknown
+	ghsas, cves := make(map[string]bool), make(map[string]bool)
+	for _, ref := range refs {
+		if ref.Type != osv.ReferenceTypeAdvisory {
+			continue
+		}
+		t, alias := re.AdvisoryType(ref.URL)
+		if t > bestType {
+			bestAdvisory = ref.URL
+			bestType = t
+		}
+
+		if ghsa.IsGHSA(alias) {
+			ghsas[alias] = true
+		} else if cveschema5.IsCVE(alias) {
+			cves[alias] = true
+		}
+	}
+
+	if len(ghsas) > 1 || len(cves) > 1 {
+		return "", false
+	}
+
+	return bestAdvisory, bestAdvisory != ""
+}
+
+type urlType int
+
+const (
+	urlTypeUnknown urlType = iota
+	urlTypeIssue
+	urlTypeFix
+	urlTypeAdvisory
+)
+
+func (re *reportRE) Type(url string) urlType {
+	switch {
+	case re.ghsa.MatchString(url):
+		return urlTypeAdvisory
+	case re.ghsaRepo.MatchString(url):
+		return urlTypeAdvisory
+	case re.cve.MatchString(url):
+		return urlTypeAdvisory
+	case re.issue.MatchString(url):
+		return urlTypeIssue
+	case re.fix.MatchString(url):
+		return urlTypeFix
+	}
+	return urlTypeUnknown
+}
+
+type advisoryType int
+
+// Advisory link types in ascending order of (likely) quality.
+// In general, repo-level GHSAs tend to be the best because
+// they are more likely to be directly created by a maintainer.
+const (
+	advisoryTypeUnknown advisoryType = iota
+	advisoryTypeCVE
+	advisoryTypeGHSA
+	advisoryTypeGHSARepo
+)
+
+func (re *reportRE) AdvisoryType(url string) (t advisoryType, alias string) {
+	if m := re.ghsa.FindStringSubmatch(url); len(m) == 2 {
+		return advisoryTypeGHSA, m[1]
+	}
+
+	if m := re.ghsaRepo.FindStringSubmatch(url); len(m) == 2 {
+		return advisoryTypeGHSARepo, m[1]
+	}
+
+	if m := re.cve.FindStringSubmatch(url); len(m) == 2 {
+		return advisoryTypeCVE, m[1]
+	}
+
+	return advisoryTypeUnknown, ""
+}
+
+type reportRE struct {
+	ghsa, ghsaRepo, cve, issue, fix *regexp.Regexp
+}
+
+func newRE(r *Report) *reportRE {
+	oneOfRE := func(s []string) string {
+		return `(` + strings.Join(s, "|") + `)`
+	}
+
+	// For now, this will not attempt to fix reference types for
+	// modules whose canonical names are different from their github path.
+	var modulePaths []string
+	for _, m := range r.Modules {
+		modulePaths = append(modulePaths, m.Module)
+	}
+	moduleRE := oneOfRE(modulePaths)
+
+	return &reportRE{
+		ghsa:     regexp.MustCompile(`^https://github.com/advisories/` + oneOfRE(r.GHSAs) + `$`),
+		ghsaRepo: regexp.MustCompile(`^https://github.com/.+/advisories/` + oneOfRE(r.GHSAs) + `$`),
+		cve:      regexp.MustCompile(`^https://nvd.nist.gov/vuln/detail/` + oneOfRE(r.CVEs) + `$`),
+		issue:    regexp.MustCompile(`https://` + moduleRE + `/issue(s?)/.*$`),
+		fix:      regexp.MustCompile(`https://` + moduleRE + `/(commit(s?)|pull)/.*$`),
+	}
+}
diff --git a/internal/report/fix_test.go b/internal/report/fix_test.go
index 0ec118c..eee8dd1 100644
--- a/internal/report/fix_test.go
+++ b/internal/report/fix_test.go
@@ -8,6 +8,7 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"golang.org/x/vulndb/internal/osv"
 	"golang.org/x/vulndb/internal/proxy"
 )
 
@@ -52,6 +53,13 @@
 		Summary: "Vulnerability in golang.org/x/vulndb",
 		Modules: []*Module{
 			{
+				Module: "golang.org/x/vulndb",
+				Versions: []VersionRange{{
+					Introduced: "0.0.0-20230522180520-0cbf4ffdb4e7",
+				}},
+				VulnerableAt: "0.0.0-20230522180520-0cbf4ffdb4e7",
+			},
+			{
 				Module: "std",
 				Versions: []VersionRange{
 					{
@@ -68,13 +76,6 @@
 				},
 				VulnerableAt: "1.20.0",
 			},
-			{
-				Module: "golang.org/x/vulndb",
-				Versions: []VersionRange{{
-					Introduced: "0.0.0-20230522180520-0cbf4ffdb4e7",
-				}},
-				VulnerableAt: "0.0.0-20230522180520-0cbf4ffdb4e7",
-			},
 		},
 		Description: "A long form description of the problem that will be broken up into multiple\nlines so it is more readable.",
 		References: []*Reference{
@@ -222,3 +223,374 @@
 		})
 	}
 }
+
+// To update proxy responses:
+// go test ./internal/report/... -proxy -run TestFixModules
+func TestFixModules(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		desc string
+		in   []*Module
+		want []*Module
+	}{
+		{
+			name: "ok",
+			desc: "module is already OK",
+			in: []*Module{{
+				Module: "github.com/influxdata/influxdb",
+				Versions: []VersionRange{
+					{
+						Introduced: "0.3.2",
+						Fixed:      "1.7.6",
+					}},
+				VulnerableAt: "1.7.5",
+			}},
+			want: []*Module{{
+				Module: "github.com/influxdata/influxdb",
+				Versions: []VersionRange{
+					{
+						Introduced: "0.3.2",
+						Fixed:      "1.7.6",
+					}},
+				VulnerableAt: "1.7.5",
+			}},
+		},
+		{
+			name: "import_path",
+			desc: "find module from import path",
+			in: []*Module{{
+				Module: "github.com/influxdata/influxdb/services/httpd",
+				Versions: []VersionRange{
+					{
+						Introduced: "0.3.2",
+						Fixed:      "1.7.6",
+					},
+				},
+			}},
+			want: []*Module{{
+				Module: "github.com/influxdata/influxdb",
+				Versions: []VersionRange{
+					{
+						Introduced: "0.3.2",
+						Fixed:      "1.7.6",
+					},
+				},
+				Packages: []*Package{
+					{
+						Package: "github.com/influxdata/influxdb/services/httpd",
+					},
+				},
+				VulnerableAt: "1.7.5",
+			}},
+		},
+		{
+			name: "major_version",
+			desc: "correct major version of module path",
+			in: []*Module{{
+				Module: "github.com/nats-io/nats-server",
+				Versions: []VersionRange{
+					{
+						Introduced: "2.2.0",
+						Fixed:      "2.8.3",
+					},
+				},
+			}},
+			want: []*Module{{
+				Module: "github.com/nats-io/nats-server/v2",
+				Versions: []VersionRange{
+					{
+						Introduced: "2.2.0",
+						Fixed:      "2.8.3",
+					},
+				},
+				VulnerableAt: "2.8.2",
+			}},
+		},
+		{
+			name: "canonicalize",
+			desc: "canonicalize module path",
+			in: []*Module{{
+				Module: "github.com/golang/vulndb",
+				Versions: []VersionRange{
+					{
+						Fixed: "0.0.0-20230712151357-4fee11d0f8f9",
+					},
+				},
+			}},
+			want: []*Module{{
+				Module: "golang.org/x/vulndb",
+				Versions: []VersionRange{
+					{
+						Fixed: "0.0.0-20230712151357-4fee11d0f8f9",
+					},
+				},
+			}},
+		},
+		{
+			name: "add_incompatible",
+			desc: "add +incompatible",
+			in: []*Module{{
+				Module: "github.com/docker/docker",
+				Versions: []VersionRange{
+					{
+						Fixed: "23.0.0",
+					},
+				},
+			}},
+			want: []*Module{{
+				Module: "github.com/docker/docker",
+				Versions: []VersionRange{
+					{
+						Fixed: "23.0.0+incompatible",
+					},
+				},
+				VulnerableAt: "23.0.0-rc.4+incompatible",
+			}},
+		},
+		{
+			name: "merge_modules",
+			desc: "merge modules that are the same except for versions",
+			in: []*Module{{
+				Module: "github.com/hashicorp/go-getter/v2",
+				Versions: []VersionRange{
+					{
+						Introduced: "2.0.0",
+						Fixed:      "2.0.2",
+					},
+				},
+			},
+				{
+					Module: "github.com/hashicorp/go-getter/v2",
+					Versions: []VersionRange{
+						{
+							Introduced: "2.1.0",
+							Fixed:      "2.1.1",
+						},
+					},
+				},
+			},
+			want: []*Module{{
+				Module: "github.com/hashicorp/go-getter/v2",
+				Versions: []VersionRange{
+					{
+						Introduced: "2.0.0",
+						Fixed:      "2.0.2",
+					},
+					{
+						Introduced: "2.1.0",
+						Fixed:      "2.1.1",
+					},
+				},
+				VulnerableAt: "2.1.0",
+			}},
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			pc, err := proxy.NewTestClient(t, *realProxy)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			r := &Report{Modules: tc.in}
+			r.FixModules(pc)
+			got := r.Modules
+
+			if diff := cmp.Diff(tc.want, got); diff != "" {
+				t.Errorf("%s: FixModules() mismatch (-want +got)\n%s", tc.desc, diff)
+			}
+		})
+	}
+}
+
+func TestFixReferences(t *testing.T) {
+	for _, tc := range []struct {
+		name     string
+		in, want []*Reference
+	}{
+		{
+			// GHSA references are converted to advisory type
+			name: "to_advisory_ghsa",
+			in: []*Reference{
+				{
+					URL:  "https://github.com/example/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/other/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
+					Type: osv.ReferenceTypeWeb,
+				},
+			},
+			want: []*Reference{
+				{
+					URL:  "https://github.com/example/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+				{
+					URL:  "https://github.com/other/module/security/advisories/GHSA-xxxx-yyyy-zzzz",
+					Type: osv.ReferenceTypeAdvisory, // different module OK, because GHSA matches
+				},
+			},
+		},
+		{
+			// CVE references are converted to advisory type
+			name: "to_advisory_cve",
+			in: []*Reference{
+				{
+					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-0001",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-2222",
+					Type: osv.ReferenceTypeWeb,
+				},
+			},
+			want: []*Reference{
+				{
+					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-0001",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+				{
+					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-2222",
+					Type: osv.ReferenceTypeWeb, // different CVE, keep "web" type
+				},
+			},
+		},
+		{
+			// CVE references are removed if GHSA is present
+			name: "remove_cve",
+			in: []*Reference{
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://nvd.nist.gov/vuln/detail/CVE-1999-0001",
+					Type: osv.ReferenceTypeWeb,
+				},
+			},
+			want: []*Reference{
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+			},
+		},
+		{
+			name: "to_fix_or_report",
+			in: []*Reference{
+				{
+					URL:  "https://github.com/example/module/pull/123",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/example/module/commit/123",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/module/module/issues/123",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/example/module/issue/123",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/different/module/issue/123",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+			},
+			want: []*Reference{
+				{
+					URL:  "https://github.com/example/module/pull/123",
+					Type: osv.ReferenceTypeFix,
+				},
+				{
+					URL:  "https://github.com/example/module/commit/123",
+					Type: osv.ReferenceTypeFix,
+				},
+				{
+					URL:  "https://github.com/module/module/issues/123",
+					Type: osv.ReferenceTypeReport,
+				},
+				{
+					URL:  "https://github.com/example/module/issue/123",
+					Type: osv.ReferenceTypeReport,
+				},
+				{
+					URL:  "https://github.com/different/module/issue/123",
+					Type: osv.ReferenceTypeWeb, // different module, keep web type
+				},
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+			},
+		},
+		{
+			// package references and go advisory references are deleted
+			name: "delete",
+			in: []*Reference{
+				{
+					URL:  "https://pkg.go.dev/vuln/GO-0000-0000",
+					Type: osv.ReferenceTypeWeb,
+				},
+				{
+					URL:  "https://github.com/anything",
+					Type: osv.ReferenceTypePackage,
+				},
+				{
+					URL:  "https://example.com",
+					Type: osv.ReferenceTypePackage,
+				},
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+			},
+			want: []*Reference{
+				{
+					URL:  "https://github.com/advisories/GHSA-gggg-hhhh-ffff",
+					Type: osv.ReferenceTypeAdvisory,
+				},
+			},
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			r := &Report{
+				Modules: []*Module{
+					{
+						Module: "github.com/example/module",
+					},
+					{
+						Module: "github.com/module/module",
+					},
+				},
+				GHSAs:      []string{"GHSA-xxxx-yyyy-zzzz", "GHSA-gggg-hhhh-ffff"},
+				CVEs:       []string{"CVE-1999-0001", "CVE-1999-0002"},
+				References: tc.in,
+			}
+			r.FixReferences()
+			got := r.References
+			if diff := cmp.Diff(tc.want, got); diff != "" {
+				t.Errorf("FixReferences() mismatch (-want +got)\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/internal/report/ghsa.go b/internal/report/ghsa.go
index e5cb7a8..679216d 100644
--- a/internal/report/ghsa.go
+++ b/internal/report/ghsa.go
@@ -9,17 +9,13 @@
 	"strings"
 
 	"golang.org/x/vulndb/internal/ghsa"
-	"golang.org/x/vulndb/internal/proxy"
 )
 
 // ghsaToReport creates a Report struct from a given GHSA SecurityAdvisory and modulePath.
-func ghsaToReport(sa *ghsa.SecurityAdvisory, modulePath string, pc *proxy.Client) *Report {
+func ghsaToReport(sa *ghsa.SecurityAdvisory, modulePath string) *Report {
 	r := &Report{
 		Summary:     Summary(sa.Summary),
 		Description: Description(sa.Description),
-		SourceMeta: &SourceMeta{
-			ID: sa.ID,
-		},
 	}
 	var cves, ghsas []string
 	for _, id := range sa.Identifiers {
@@ -48,7 +44,6 @@
 		}
 		r.Modules = append(r.Modules, m)
 	}
-	r.Fix(pc)
 	return r
 }
 
diff --git a/internal/report/ghsa_test.go b/internal/report/ghsa_test.go
index 3e775a4..2138a60 100644
--- a/internal/report/ghsa_test.go
+++ b/internal/report/ghsa_test.go
@@ -63,20 +63,24 @@
 			},
 		},
 		{
-			name:   "empty module uses package as module",
+			name:   "empty module attempts to find module from package",
 			module: "",
 			want: &Report{
 				ID: placeholderID,
 				Modules: []*Module{{
-					Module: "golang.org/x/tools/go/packages",
+					Module: "golang.org/x/tools",
 					Versions: []VersionRange{
 						{Fixed: "0.9.0"},
 					},
+					VulnerableAt: "0.8.0",
 					Packages: []*Package{{
 						Package: "golang.org/x/tools/go/packages",
-					}},
+					},
+						{
+							Package: "golang.org/x/tools/go/packages",
+						}},
 				}},
-				Summary:     "C1 in golang.org/x/tools/go/packages",
+				Summary:     "C1 in golang.org/x/tools",
 				Description: "a description",
 				GHSAs:       []string{"G1"},
 				CVEs:        []string{"C1"},
diff --git a/internal/report/new.go b/internal/report/new.go
index 7b78d04..0e94cb9 100644
--- a/internal/report/new.go
+++ b/internal/report/new.go
@@ -20,11 +20,18 @@
 	// SourceID returns the ID of the source.
 	// For example, the GHSA or CVE id.
 	SourceID() string
-	ToReport(goID string, modulePath string, pc *proxy.Client) *Report
+	ToReport(modulePath string) *Report
 }
 
 func New(src Source, goID string, modulePath string, pc *proxy.Client) *Report {
-	return src.ToReport(goID, modulePath, pc)
+	r := src.ToReport(modulePath)
+	r.ID = goID
+	r.SourceMeta = &SourceMeta{
+		ID: src.SourceID(),
+	}
+
+	r.Fix(pc)
+	return r
 }
 
 type Fetcher interface {
@@ -41,8 +48,8 @@
 	return &cve5{CVERecord: c}
 }
 
-func (c *cve5) ToReport(goID, modulePath string, pc *proxy.Client) *Report {
-	return cve5ToReport(c.CVERecord, goID, modulePath, pc)
+func (c *cve5) ToReport(modulePath string) *Report {
+	return cve5ToReport(c.CVERecord, modulePath)
 }
 
 func (c *cve5) SourceID() string {
@@ -77,8 +84,8 @@
 	return &cve4{CVE: c}
 }
 
-func (c *cve4) ToReport(goID, modulePath string, pc *proxy.Client) *Report {
-	return cveToReport(c.CVE, goID, modulePath, pc)
+func (c *cve4) ToReport(modulePath string) *Report {
+	return cveToReport(c.CVE, modulePath)
 }
 
 func (c *cve4) SourceID() string {
@@ -102,10 +109,8 @@
 	}
 }
 
-func (g *legacyGHSA) ToReport(goID, modulePath string, pc *proxy.Client) *Report {
-	r := ghsaToReport(g.SecurityAdvisory, modulePath, pc)
-	r.ID = goID
-	return r
+func (g *legacyGHSA) ToReport(modulePath string) *Report {
+	return ghsaToReport(g.SecurityAdvisory, modulePath)
 }
 
 func (g *legacyGHSA) SourceID() string {
@@ -144,17 +149,13 @@
 	return &original{}
 }
 
-func (original) ToReport(goID, modulePath string, _ *proxy.Client) *Report {
+func (original) ToReport(modulePath string) *Report {
 	return &Report{
-		ID: goID,
 		Modules: []*Module{
 			{
 				Module: modulePath,
 			},
 		},
-		SourceMeta: &SourceMeta{
-			ID: sourceGoTeam,
-		},
 	}
 }
 
diff --git a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-29407.txtar b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-29407.txtar
index 47ef77f..ae6b582 100644
--- a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-29407.txtar
+++ b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-29407.txtar
@@ -29,7 +29,6 @@
 references:
     - report: https://go.dev/issue/61581
     - fix: https://go.dev/cl/514897
-    - web: https://pkg.go.dev/vuln/GO-2023-1990
     - web: https://security.netapp.com/advisory/ntap-20230831-0009/
     - web: https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KO54NBDUJXKAZNGCFOEYL2LKK2RQP6K6/
     - web: https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XWH6Q7NVM4MV3GWFEU4PA67AWZHVFJQ2/
diff --git a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-44378.txtar b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-44378.txtar
index 5ff54bd..1200420 100644
--- a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-44378.txtar
+++ b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-44378.txtar
@@ -7,14 +7,14 @@
 -- CVE-2023-44378 --
 id: PLACEHOLDER-ID
 modules:
-    - module: github.com/Consensys/gnark
+    - module: github.com/consensys/gnark
       versions:
         - fixed: 0.9.0
       packages:
-        - package: github.com/Consensys/gnark
+        - package: github.com/consensys/gnark
 summary: |-
     gnark vulnerable to unsoundness in variable comparison/non-unique binary
-    decomposition in github.com/Consensys/gnark
+    decomposition in github.com/consensys/gnark
 description: |-
     gnark is a zk-SNARK library that offers a high-level API to design circuits.
     Prior to version 0.9.0, for some in-circuit values, it is possible to construct
diff --git a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45283.txtar b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45283.txtar
index fc4217c..b87d895 100644
--- a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45283.txtar
+++ b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45283.txtar
@@ -13,6 +13,22 @@
         - introduced: 1.21.0-0
           fixed: 1.21.4
       packages:
+        - package: internal/safefilepath
+          goos:
+            - windows
+          symbols:
+            - fromFS
+            - FromFS
+    - module: std
+      versions:
+        - fixed: 1.20.11
+        - introduced: 1.20.11
+          fixed: 1.20.12
+        - introduced: 1.21.0-0
+          fixed: 1.21.4
+        - introduced: 1.21.4
+          fixed: 1.21.5
+      packages:
         - package: path/filepath
           goos:
             - windows
@@ -32,43 +48,6 @@
             - VolumeName
             - Walk
             - WalkDir
-    - module: std
-      versions:
-        - fixed: 1.20.11
-        - introduced: 1.21.0-0
-          fixed: 1.21.4
-      packages:
-        - package: internal/safefilepath
-          goos:
-            - windows
-          symbols:
-            - fromFS
-            - FromFS
-    - module: std
-      versions:
-        - introduced: 1.20.11
-          fixed: 1.20.12
-        - introduced: 1.21.4
-          fixed: 1.21.5
-      packages:
-        - package: path/filepath
-          goos:
-            - windows
-          symbols:
-            - volumeNameLen
-            - Abs
-            - Base
-            - Clean
-            - Dir
-            - EvalSymlinks
-            - Glob
-            - IsLocal
-            - Join
-            - Rel
-            - Split
-            - VolumeName
-            - Walk
-            - WalkDir
 summary: Insecure parsing of Windows paths with a \??\ prefix in path/filepath
 description: |-
     The filepath package does not recognize paths with a \??\ prefix as special. On
@@ -92,7 +71,6 @@
     - report: https://go.dev/issue/64028
     - fix: https://go.dev/cl/541175
     - web: https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ
-    - web: https://pkg.go.dev/vuln/GO-2023-2185
     - web: http://www.openwall.com/lists/oss-security/2023/12/05/2
 cve_metadata:
     id: CVE-2023-45283
diff --git a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45285.txtar b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45285.txtar
index 834bff6..de106a6 100644
--- a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45285.txtar
+++ b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45285.txtar
@@ -27,7 +27,6 @@
     - web: https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ
     - report: https://go.dev/issue/63845
     - fix: https://go.dev/cl/540257
-    - web: https://pkg.go.dev/vuln/GO-2023-2383
 cve_metadata:
     id: CVE-2023-45285
     cwe: 'CWE-636: Not Failing Securely (''Failing Open'')'
diff --git a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45286.txtar b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45286.txtar
index a7651fc..f4e6c45 100644
--- a/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45286.txtar
+++ b/internal/report/testdata/cve/TestCVE5ToReport/CVE-2023-45286.txtar
@@ -43,7 +43,6 @@
     - report: https://github.com/go-resty/resty/issues/743
     - report: https://github.com/go-resty/resty/issues/739
     - fix: https://github.com/go-resty/resty/pull/745
-    - web: https://pkg.go.dev/vuln/GO-2023-2328
 cve_metadata:
     id: CVE-2023-45286
     cwe: 'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor'
diff --git a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-29407.txtar b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-29407.txtar
index 959c21e..e69fbed 100644
--- a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-29407.txtar
+++ b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-29407.txtar
@@ -19,7 +19,6 @@
 references:
     - report: https://go.dev/issue/61581
     - fix: https://go.dev/cl/514897
-    - web: https://pkg.go.dev/vuln/GO-2023-1990
     - web: https://security.netapp.com/advisory/ntap-20230831-0009/
     - web: https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KO54NBDUJXKAZNGCFOEYL2LKK2RQP6K6/
     - web: https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XWH6Q7NVM4MV3GWFEU4PA67AWZHVFJQ2/
diff --git a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45283.txtar b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45283.txtar
index 7605417..9c474c7 100644
--- a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45283.txtar
+++ b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45283.txtar
@@ -33,7 +33,6 @@
     - report: https://go.dev/issue/64028
     - fix: https://go.dev/cl/541175
     - web: https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ
-    - web: https://pkg.go.dev/vuln/GO-2023-2185
     - web: http://www.openwall.com/lists/oss-security/2023/12/05/2
 cve_metadata:
     id: CVE-2023-45283
diff --git a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45285.txtar b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45285.txtar
index da94dec..10c3672 100644
--- a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45285.txtar
+++ b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45285.txtar
@@ -21,7 +21,6 @@
     - web: https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ
     - report: https://go.dev/issue/63845
     - fix: https://go.dev/cl/540257
-    - web: https://pkg.go.dev/vuln/GO-2023-2383
 cve_metadata:
     id: CVE-2023-45285
     cwe: 'CWE-636: Not Failing Securely (''Failing Open'')'
diff --git a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45286.txtar b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45286.txtar
index ad6fd36..f1d1fb3 100644
--- a/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45286.txtar
+++ b/internal/report/testdata/cve/TestCVEToReport/CVE-2023-45286.txtar
@@ -26,7 +26,6 @@
     - report: https://github.com/go-resty/resty/issues/743
     - report: https://github.com/go-resty/resty/issues/739
     - fix: https://github.com/go-resty/resty/pull/745
-    - web: https://pkg.go.dev/vuln/GO-2023-2328
 cve_metadata:
     id: CVE-2023-45286
     cwe: 'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor'
diff --git a/internal/report/testdata/proxy/TestFixModules/add_incompatible.json b/internal/report/testdata/proxy/TestFixModules/add_incompatible.json
new file mode 100644
index 0000000..055b115
--- /dev/null
+++ b/internal/report/testdata/proxy/TestFixModules/add_incompatible.json
@@ -0,0 +1,10 @@
+{
+	"github.com/docker/docker/@v/list": {
+		"body": "v1.9.0\nv1.6.0-rc3\nv1.3.0\nv1.12.0-rc3\nv1.8.2-rc1\nv20.10.11+incompatible\nv24.0.3+incompatible\nv1.7.0\nv23.0.0-rc.3+incompatible\nv1.7.0-rc5\nv0.1.4\nv25.0.1+incompatible\nv1.12.2-rc3\nv23.0.9+incompatible\nv1.11.0-rc3\nv1.13.0-rc2\nv23.0.3+incompatible\nv20.10.21+incompatible\nv17.11.0-ce-rc3+incompatible\nv25.0.0-rc.1+incompatible\nv17.10.0-ce+incompatible\nv1.12.0-rc2\nv0.7.5\nv0.2.2\nv0.7.0-rc1\nv1.6.0-rc7\nv0.7.0\nv1.13.1-rc1\nv1.10.0-rc3\nv25.0.5+incompatible\nv0.1.3\nv20.10.6+incompatible\nv0.8.1\nv26.0.2+incompatible\nv1.13.1\nv20.10.1+incompatible\nv0.7.3\nv17.12.1-ce+incompatible\nv1.8.0\nv20.10.0+incompatible\nv1.12.0-rc4\nv0.6.6\nv20.10.4+incompatible\nv1.9.0-rc2\nv1.12.2-rc2\nv0.4.5\nv0.3.0\nv0.4.6\nv1.6.0\nv0.5.2\nv26.0.0-rc1+incompatible\nv0.6.1\nv20.10.23+incompatible\nv0.3.4\nv1.11.2\nv1.0.0\nv0.7.2\nv0.8.0\nv23.0.0-rc.2+incompatible\nv1.8.3\nv24.0.1+incompatible\nv23.0.8+incompatible\nv1.7.0-rc1\nv1.12.0\nv0.4.3\nv0.6.0\nv20.10.10-rc1+incompatible\nv23.0.2+incompatible\nv1.3.3\nv24.0.2+incompatible\nv1.13.0-rc6\nv1.11.2-rc1\nv0.10.0\nv0.7.4\nv1.12.4-rc1\nv0.5.3\nv1.8.0-rc1\nv0.7.0-rc2\nv1.5.0-rc4\nv1.6.0-rc2\nv0.6.2\nv24.0.0+incompatible\nv23.0.0+incompatible\nv1.7.0-rc3\nv20.10.15+incompatible\nv17.10.0-ce-rc2+incompatible\nv20.10.17+incompatible\nv0.5.0\nv24.0.0-rc.2+incompatible\nv1.13.1-rc2\nv1.8.2\nv20.10.16+incompatible\nv1.9.0-rc1\nv20.10.2+incompatible\nv20.10.22+incompatible\nv1.1.1\nv20.10.20+incompatible\nv1.12.6\nv20.10.0-rc2+incompatible\nv1.12.1\nv25.0.2+incompatible\nv20.10.27+incompatible\nv1.5.0-rc2\nv1.3.1\nv0.1.7\nv17.12.0-ce-rc1+incompatible\nv1.5.0\nv23.0.7+incompatible\nv17.11.0-ce+incompatible\nv23.0.6+incompatible\nv1.11.0-rc5\nv1.11.0-rc4\nv0.12.0\nv0.1.6\nv17.11.0-ce-rc1+incompatible\nv1.7.1\nv1.4.1\nv20.10.9+incompatible\nv1.7.1-rc1\nv17.12.0-ce-rc3+incompatible\nv1.6.0-rc6\nv25.0.0-beta.3+incompatible\nv1.5.0-rc1\nv0.7.0-rc5\nv1.1.2\nv20.10.18+incompatible\nv1.9.1\nv17.11.0-ce-rc2+incompatible\nv1.12.3\nv1.10.0\nv17.12.1-ce-rc2+incompatible\nv20.10.26+incompatible\nv1.8.0-rc3\nv0.1.2\nv0.3.3\nv24.0.0-rc.4+incompatible\nv0.4.4\nv20.10.14+incompatible\nv0.7.0-rc4\nv0.9.0\nv0.6.5\nv0.4.0\nv25.0.0-beta.1+incompatible\nv0.1.8\nv0.4.1\nv20.10.8+incompatible\nv1.11.0-rc2\nv1.7.1-rc3\nv23.0.0-rc.4+incompatible\nv25.0.0+incompatible\nv20.10.0-rc1+incompatible\nv0.5.1\nv0.7.0-rc7\nv24.0.0-beta.2+incompatible\nv1.9.1-rc1\nv1.11.0\nv23.0.1+incompatible\nv24.0.6+incompatible\nv23.0.5+incompatible\nv20.10.3+incompatible\nv1.10.1-rc1\nv1.12.5\nv1.6.0-rc4\nv25.0.0-rc.3+incompatible\nv25.0.3+incompatible\nv1.10.2\nv1.13.0-rc5\nv24.0.0-rc.1+incompatible\nv24.0.5+incompatible\nv1.3.2\nv17.12.0-ce-rc2+incompatible\nv0.3.1\nv0.2.1\nv0.11.0\nv1.10.2-rc1\nv1.7.0-rc2\nv0.1.0\nv17.12.0-ce-rc4+incompatible\nv1.11.1-rc1\nv20.10.10+incompatible\nv1.4.0\nv0.7.1\nv0.1.5\nv1.13.0-rc1\nv0.9.1\nv1.2.0\nv1.13.0-rc7\nv0.6.3\nv0.1.1\nv24.0.0-beta.1+incompatible\nv1.12.1-rc2\nv25.0.0-beta.2+incompatible\nv20.10.0-beta1+incompatible\nv25.0.4+incompatible\nv0.4.7\nv1.12.2\nv1.10.3-rc2\nv1.9.0-rc4\nv1.10.0-rc2\nv20.10.5+incompatible\nv20.10.13+incompatible\nv1.6.1\nv1.13.0-rc3\nv1.12.4\nv1.10.1\nv0.7.0-rc6\nv26.0.0+incompatible\nv1.13.0-rc4\nv0.7.6\nv1.12.5-rc1\nv24.0.4+incompatible\nv1.12.3-rc1\nv0.11.1\nv1.7.0-rc4\nv26.0.0-rc3+incompatible\nv1.12.1-rc1\nv25.0.0-rc.2+incompatible\nv1.7.1-rc2\nv1.10.3-rc1\nv0.6.7\nv1.13.0\nv24.0.9+incompatible\nv1.6.0-rc1\nv26.0.0-rc2+incompatible\nv1.10.0-rc4\nv1.8.1\nv1.10.3\nv24.0.7+incompatible\nv1.12.0-rc5\nv1.6.2\nv1.9.0-rc3\nv17.10.0-ce-rc1+incompatible\nv24.0.8+incompatible\nv1.0.1\nv1.1.0\nv17.11.0-ce-rc4+incompatible\nv20.10.24+incompatible\nv0.6.4\nv1.11.1\nv1.11.0-rc1\nv1.12.0-rc1\nv1.5.0-rc3\nv0.2.0\nv1.8.0-rc2\nv1.10.0-rc1\nv0.4.2\nv23.0.0-beta.1+incompatible\nv20.10.25+incompatible\nv23.0.10+incompatible\nv17.12.1-ce-rc1+incompatible\nv24.0.0-rc.3+incompatible\nv20.10.7+incompatible\nv0.3.2\nv23.0.0-rc.1+incompatible\nv0.4.8\nv1.9.0-rc5\nv0.7.0-rc3\nv20.10.12+incompatible\nv17.12.0-ce+incompatible\nv1.6.0-rc5\nv20.10.19+incompatible\nv23.0.4+incompatible\nv26.0.1+incompatible\nv1.12.2-rc1\n",
+		"status_code": 200
+	},
+	"github.com/docker/docker/@v/v23.0.0+incompatible.mod": {
+		"body": "module github.com/docker/docker\n",
+		"status_code": 200
+	}
+}
\ No newline at end of file
diff --git a/internal/genericosv/testdata/proxy/TestAffectedToModules/canonicalize.json b/internal/report/testdata/proxy/TestFixModules/canonicalize.json
similarity index 98%
rename from internal/genericosv/testdata/proxy/TestAffectedToModules/canonicalize.json
rename to internal/report/testdata/proxy/TestFixModules/canonicalize.json
index eced014..5186031 100644
--- a/internal/genericosv/testdata/proxy/TestAffectedToModules/canonicalize.json
+++ b/internal/report/testdata/proxy/TestFixModules/canonicalize.json
@@ -6,6 +6,9 @@
 		"body": "module golang.org/x/vulndb\n\ngo 1.18\n\nrequire golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect\n\nrequire (\n\tcloud.google.com/go/errorreporting v0.1.0\n\tcloud.google.com/go/firestore v1.6.1\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go v1.0.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.26.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.0.0\n\tgithub.com/client9/misspell v0.3.4\n\tgithub.com/go-git/go-billy/v5 v5.4.1\n\tgithub.com/go-git/go-git/v5 v5.7.0\n\tgithub.com/google/go-cmp v0.5.9\n\tgithub.com/google/go-github/v41 v41.0.0\n\tgithub.com/google/osv-scanner v1.3.4\n\tgithub.com/google/safehtml v0.0.2\n\tgithub.com/jba/templatecheck v0.6.0\n\tgithub.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2\n\tgo.opentelemetry.io/otel v1.4.0\n\tgo.opentelemetry.io/otel/sdk v1.4.0\n\tgolang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1\n\tgolang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e\n\tgolang.org/x/mod v0.10.0\n\tgolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8\n\tgolang.org/x/sync v0.2.0\n\tgolang.org/x/time v0.0.0-20191024005414-555d28b269f0\n\tgolang.org/x/tools v0.9.3\n\tgoogle.golang.org/api v0.70.0\n\tgoogle.golang.org/grpc v1.44.0\n\tgopkg.in/yaml.v3 v3.0.1\n\thonnef.co/go/tools v0.2.2\n\tmvdan.cc/unparam v0.0.0-20220926085101-66de63301820\n)\n\nrequire (\n\tcloud.google.com/go v0.100.2 // indirect\n\tcloud.google.com/go/compute v1.3.0 // indirect\n\tcloud.google.com/go/monitoring v1.2.0 // indirect\n\tcloud.google.com/go/trace v1.0.0 // indirect\n\tgithub.com/BurntSushi/toml v1.3.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.5.2 // indirect\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect\n\tgithub.com/acomagu/bufpipe v1.0.4 // indirect\n\tgithub.com/cloudflare/circl v1.3.3 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-logr/logr v1.2.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.1.1 // indirect\n\tgithub.com/imdario/mergo v0.3.15 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/pjbgf/sha1cd v0.3.0 // indirect\n\tgithub.com/sergi/go-diff v1.1.0 // indirect\n\tgithub.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect\n\tgithub.com/skeema/knownhosts v1.1.1 // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgo.opencensus.io v0.23.0 // indirect\n\tgo.opentelemetry.io/otel/internal/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/export/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.4.0 // indirect\n\tgolang.org/x/crypto v0.9.0 // indirect\n\tgolang.org/x/net v0.10.0 // indirect\n\tgolang.org/x/sys v0.8.0 // indirect\n\tgolang.org/x/text v0.9.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect\n\tgoogle.golang.org/protobuf v1.27.1 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n)\n",
 		"status_code": 200
 	},
+	"golang.org/x/vulndb/@v/list": {
+		"status_code": 200
+	},
 	"golang.org/x/vulndb/@v/v0.0.0-20230712151357-4fee11d0f8f9.mod": {
 		"body": "module golang.org/x/vulndb\n\ngo 1.18\n\nrequire golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect\n\nrequire (\n\tcloud.google.com/go/errorreporting v0.1.0\n\tcloud.google.com/go/firestore v1.6.1\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go v1.0.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.26.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.0.0\n\tgithub.com/client9/misspell v0.3.4\n\tgithub.com/go-git/go-billy/v5 v5.4.1\n\tgithub.com/go-git/go-git/v5 v5.7.0\n\tgithub.com/google/go-cmp v0.5.9\n\tgithub.com/google/go-github/v41 v41.0.0\n\tgithub.com/google/osv-scanner v1.3.4\n\tgithub.com/google/safehtml v0.0.2\n\tgithub.com/jba/templatecheck v0.6.0\n\tgithub.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2\n\tgo.opentelemetry.io/otel v1.4.0\n\tgo.opentelemetry.io/otel/sdk v1.4.0\n\tgolang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1\n\tgolang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e\n\tgolang.org/x/mod v0.10.0\n\tgolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8\n\tgolang.org/x/sync v0.2.0\n\tgolang.org/x/time v0.0.0-20191024005414-555d28b269f0\n\tgolang.org/x/tools v0.9.3\n\tgoogle.golang.org/api v0.70.0\n\tgoogle.golang.org/grpc v1.44.0\n\tgopkg.in/yaml.v3 v3.0.1\n\thonnef.co/go/tools v0.2.2\n\tmvdan.cc/unparam v0.0.0-20220926085101-66de63301820\n)\n\nrequire (\n\tcloud.google.com/go v0.100.2 // indirect\n\tcloud.google.com/go/compute v1.3.0 // indirect\n\tcloud.google.com/go/monitoring v1.2.0 // indirect\n\tcloud.google.com/go/trace v1.0.0 // indirect\n\tgithub.com/BurntSushi/toml v1.3.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.5.2 // indirect\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect\n\tgithub.com/acomagu/bufpipe v1.0.4 // indirect\n\tgithub.com/cloudflare/circl v1.3.3 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-logr/logr v1.2.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.1.1 // indirect\n\tgithub.com/imdario/mergo v0.3.15 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/pjbgf/sha1cd v0.3.0 // indirect\n\tgithub.com/sergi/go-diff v1.1.0 // indirect\n\tgithub.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect\n\tgithub.com/skeema/knownhosts v1.1.1 // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgo.opencensus.io v0.23.0 // indirect\n\tgo.opentelemetry.io/otel/internal/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/export/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.4.0 // indirect\n\tgolang.org/x/crypto v0.9.0 // indirect\n\tgolang.org/x/net v0.10.0 // indirect\n\tgolang.org/x/sys v0.8.0 // indirect\n\tgolang.org/x/text v0.9.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect\n\tgoogle.golang.org/protobuf v1.27.1 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n)\n",
 		"status_code": 200
diff --git a/internal/report/testdata/proxy/TestFixModules/import_path.json b/internal/report/testdata/proxy/TestFixModules/import_path.json
new file mode 100644
index 0000000..e36dba7
--- /dev/null
+++ b/internal/report/testdata/proxy/TestFixModules/import_path.json
@@ -0,0 +1,20 @@
+{
+	"github.com/influxdata/influxdb/@v/list": {
+		"body": "v1.9.0\nv0.8.0-rc.2\nv1.3.0\nv1.0.2\nv0.5.8\nv0.9.0-rc22\nv0.9.0-rc21\nv0.9.5-rc3\nv1.7.0\nv1.8.8\nv0.9.0-rc23\nv1.0.0-beta3\nv0.10.0-rc2\nv0.9.0-rc3\nv0.5.5\nv0.5.0-rc.4\nv1.2.3\nv0.9.3\nv0.7.0\nv1.7.9\nv1.1.0-rc2\nv0.9.0-rc33\nv0.8.1\nv0.9.0-rc24\nv1.9.6\nv0.9.0-rc18\nv0.9.2\nv0.7.3\nv1.9.8\nv1.5.1\nv1.8.0\nv1.2.1-rc4\nv1.6.4\nv0.9.0-rc13\nv0.9.0-rc29\nv0.9.0-rc14\nv1.6.5\nv0.5.0-rc.3\nv0.8.8\nv0.9.0-rc10\nv0.3.0\nv1.7.9-dev1\nv1.7.8\nv0.11.0-rc1\nv1.6.0\nv0.5.2\nv1.4.2\nv0.6.1\nv1.11.2\nv0.9.0-rc19\nv1.9.7\nv1.0.0\nv0.7.2\nv1.8.10\nv0.8.0\nv0.9.0-rc4\nv1.8.3\nv0.9.0-rc12\nv0.9.0-rc27\nv0.5.11\nv1.4.3\nv0.8.3\nv0.4.3\nv1.8.5\nv0.6.0\nv0.9.0-rc31\nv0.0.1\nv0.9.1-rc2\nv0.5.0-rc.6\nv1.3.3\nv0.10.0-beta2\nv0.5.6\nv0.10.0-beta1\nv0.10.0\nv1.8.7\nv0.5.3\nv1.3.6\nv0.10.2\nv1.1.0-rc1\nv0.6.2\nv0.5.0-rc.2\nv1.8.9\nv0.5.0\nv0.9.0-rc9\nv1.8.2\nv0.9.0-rc6\nv1.11.4\nv1.5.5\nv0.9.0-rc7\nv1.1.1\nv0.5.0-rc.5\nv0.8.0-rc.4\nv0.9.6-rc1\nv0.9.0-rc30\nv0.9.5\nv1.8.6\nv1.9.12\nv0.8.0-rc.5\nv0.10.3\nv1.3.1\nv1.0.0-beta1\nv1.5.0\nv1.2.3-gershontest\nv1.3.4\nv0.0.5\nv1.7.2\nv0.12.0\nv0.8.7\nv1.7.1\nv1.4.1\nv0.12.2\nv0.9.0-rc20\nv1.1.3\nv1.7.11\nv1.9.13\nv1.1.2\nv1.6.1-exp-2-tr\nv1.9.1\nv1.6.3\nv0.8.5\nv1.10.0\nv0.9.0-rc16\nv1.7.3\nv0.5.0-rc.1\nv0.9.2-rc1\nv0.4.4\nv0.9.0\nv0.6.5\nv0.4.0\nv1.9.10\nv0.4.1\nv0.9.0-rc28\nv0.5.4\nv0.9.0-rc25\nv0.0.3\nv1.2.0-rc1\nv1.2.1-rc2\nv0.5.1\nv1.7.5\nv1.11.0\nv0.8.4\nv0.0.4\nv1.5.2\nv0.9.4-rc1\nv1.10.2\nv1.9.4\nv1.0.0-rc1\nv1.2.4\nv1.3.2\nv1.2.1\nv1.5.4\nv0.10.1\nv1.9.3\nv1.2.0-rc2\nv0.3.1\nv1.2.1-rc3\nv0.9.6-rc2\nv0.11.0\nv1.1.5\nv0.5.7\nv0.1.0\nv0.9.3-rc1\nv1.4.0\nv0.7.1\nv0.9.5-rc1\nv0.0.2\nv1.1.4\nv0.9.1\nv1.2.0\nv0.6.3\nv0.9.6\nv1.6.1-exp-tr\nv1.10.4\nv1.9.9\nv1.2.1-rc1\nv1.3.9\nv1.9.11\nv1.2.1-rc5\nv1.2.2\nv0.0.8\nv0.9.0-rc11\nv0.0.9\nv0.10.0-rc1\nv0.9.3-rc2\nv1.6.1\nv0.9.0-rc17\nv1.7.7\nv1.10.1\nv0.13.0-rc2\nv1.5.3\nv0.8.2\nv0.11.1\nv0.9.0-rc32\nv0.5.10\nv1.10.5\nv1.3.5\nv0.13.0\nv1.9.2\nv0.5.9\nv1.6.6\nv0.8.0-rc.1\nv1.7.10\nv1.3.8\nv0.8.6\nv0.9.0-rc5\nv0.9.0-rc8\nv1.9.5\nv0.13.0-rc1\nv1.8.1\nv1.10.3\nv1.6.2\nv1.7.4\nv1.0.1\nv0.9.0-rc2\nv1.11.5\nv0.9.3-rc3\nv1.1.0\nv1.0.0-rc2\nv0.6.4\nv1.11.1\nv0.0.6\nv0.0.7\nv1.8.4\nv0.2.0\nv1.3.7\nv0.9.5-rc2\nv1.0.0-beta2\nv0.8.0-rc.3\nv0.4.2\nv0.9.0-rc15\nv0.8.9\nv0.8.4-rc.1\nv0.9.1-rc1\nv0.3.2\nv0.12.1\nv0.9.4\nv1.7.6\nv0.5.12\nv0.9.0-rc26\n",
+		"status_code": 200
+	},
+	"github.com/influxdata/influxdb/@v/v0.3.2.mod": {
+		"body": "module github.com/influxdata/influxdb\n",
+		"status_code": 200
+	},
+	"github.com/influxdata/influxdb/@v/v1.7.6.mod": {
+		"body": "module github.com/influxdata/influxdb\n",
+		"status_code": 200
+	},
+	"github.com/influxdata/influxdb/services/@v/list": {
+		"status_code": 404
+	},
+	"github.com/influxdata/influxdb/services/httpd/@v/list": {
+		"status_code": 404
+	}
+}
\ No newline at end of file
diff --git a/internal/genericosv/testdata/proxy/TestAffectedToModules/major_version.json b/internal/report/testdata/proxy/TestFixModules/major_version.json
similarity index 67%
rename from internal/genericosv/testdata/proxy/TestAffectedToModules/major_version.json
rename to internal/report/testdata/proxy/TestFixModules/major_version.json
index 1ca6bbe..7e6bb4a 100644
--- a/internal/genericosv/testdata/proxy/TestAffectedToModules/major_version.json
+++ b/internal/report/testdata/proxy/TestFixModules/major_version.json
@@ -4,7 +4,7 @@
 		"status_code": 200
 	},
 	"github.com/nats-io/nats-server/v2/@v/list": {
-		"body": "v2.8.3\nv2.3.0\nv2.2.0\nv2.9.18\nv2.8.4\nv2.9.1\nv2.6.0\nv2.0.2\nv2.9.11\nv2.6.3\nv2.9.10\nv2.9.9\nv2.2.4\nv2.7.4\nv2.1.2\nv2.0.4\nv2.6.2\nv2.7.2\nv2.9.22\nv2.2.5\nv2.6.5\nv2.9.19\nv2.0.0\nv2.9.21\nv2.9.17\nv2.2.1\nv2.7.3\nv2.3.4\nv2.4.0\nv2.2.2\nv2.7.1\nv2.8.0\nv2.3.2\nv2.9.7\nv2.2.6\nv2.7.0-rc2\nv2.7.0\nv2.1.7\nv2.9.4\nv2.1.8\nv2.9.5\nv2.9.14\nv2.9.12\nv2.8.1\nv2.9.3\nv2.0.0-RC19\nv2.1.0\nv2.3.3\nv2.9.16\nv2.9.8\nv2.1.4\nv2.3.1\nv2.9.0\nv2.9.6\nv2.6.1\nv2.9.2\nv2.1.9\nv2.9.20\nv2.1.6\nv2.9.15\nv2.2.3\nv2.7.0-rc1\nv2.0.0-RC14\nv2.8.2\nv2.6.4\nv2.5.0\nv2.6.6\n",
+		"body": "v2.8.3\nv2.10.1\nv2.3.0\nv2.2.0\nv2.9.18\nv2.8.4\nv2.9.1\nv2.6.0\nv2.10.9\nv2.0.2\nv2.9.11\nv2.10.3\nv2.6.3\nv2.11.0-dev\nv2.9.10\nv2.10.6\nv2.9.9\nv2.2.4\nv2.7.4\nv2.10.7\nv2.1.2\nv2.0.4\nv2.10.5\nv2.6.2\nv2.7.2\nv2.9.22\nv2.2.5\nv2.6.5\nv2.9.19\nv2.10.14\nv2.0.0\nv2.9.21\nv2.9.17\nv2.2.1\nv2.10.12\nv2.7.3\nv2.3.4\nv2.4.0\nv2.11.0-preview.1\nv2.10.11\nv2.2.2\nv2.7.1\nv2.8.0\nv2.3.2\nv2.9.7\nv2.2.6\nv2.7.0-rc2\nv2.10.4\nv2.7.0\nv2.1.7\nv2.9.4\nv2.1.8\nv2.9.5\nv2.9.14\nv2.9.12\nv2.9.23\nv2.9.24\nv2.8.1\nv2.9.3\nv2.0.0-RC19\nv2.1.0\nv2.3.3\nv2.9.16\nv2.9.8\nv2.9.25\nv2.1.4\nv2.10.8\nv2.10.0\nv2.3.1\nv2.10.2\nv2.9.0\nv2.9.6\nv2.6.1\nv2.9.2\nv2.1.9\nv2.9.20\nv2.1.6\nv2.9.15\nv2.2.3\nv2.7.0-rc1\nv2.0.0-RC14\nv2.8.2\nv2.6.4\nv2.5.0\nv2.6.6\nv2.10.10\n",
 		"status_code": 200
 	},
 	"github.com/nats-io/nats-server/v2/@v/v2.2.0.mod": {
diff --git a/internal/genericosv/testdata/proxy/TestAffectedToModules/merge_modules.json b/internal/report/testdata/proxy/TestFixModules/merge_modules.json
similarity index 100%
rename from internal/genericosv/testdata/proxy/TestAffectedToModules/merge_modules.json
rename to internal/report/testdata/proxy/TestFixModules/merge_modules.json
diff --git a/internal/report/testdata/proxy/TestFixModules/ok.json b/internal/report/testdata/proxy/TestFixModules/ok.json
new file mode 100644
index 0000000..db7f83f
--- /dev/null
+++ b/internal/report/testdata/proxy/TestFixModules/ok.json
@@ -0,0 +1,14 @@
+{
+	"github.com/influxdata/influxdb/@v/list": {
+		"body": "v1.9.0\nv0.8.0-rc.2\nv1.3.0\nv1.0.2\nv0.5.8\nv0.9.0-rc22\nv0.9.0-rc21\nv0.9.5-rc3\nv1.7.0\nv1.8.8\nv0.9.0-rc23\nv1.0.0-beta3\nv0.10.0-rc2\nv0.9.0-rc3\nv0.5.5\nv0.5.0-rc.4\nv1.2.3\nv0.9.3\nv0.7.0\nv1.7.9\nv1.1.0-rc2\nv0.9.0-rc33\nv0.8.1\nv0.9.0-rc24\nv1.9.6\nv0.9.0-rc18\nv0.9.2\nv0.7.3\nv1.9.8\nv1.5.1\nv1.8.0\nv1.2.1-rc4\nv1.6.4\nv0.9.0-rc13\nv0.9.0-rc29\nv0.9.0-rc14\nv1.6.5\nv0.5.0-rc.3\nv0.8.8\nv0.9.0-rc10\nv0.3.0\nv1.7.9-dev1\nv1.7.8\nv0.11.0-rc1\nv1.6.0\nv0.5.2\nv1.4.2\nv0.6.1\nv1.11.2\nv0.9.0-rc19\nv1.9.7\nv1.0.0\nv0.7.2\nv1.8.10\nv0.8.0\nv0.9.0-rc4\nv1.8.3\nv0.9.0-rc12\nv0.9.0-rc27\nv0.5.11\nv1.4.3\nv0.8.3\nv0.4.3\nv1.8.5\nv0.6.0\nv0.9.0-rc31\nv0.0.1\nv0.9.1-rc2\nv0.5.0-rc.6\nv1.3.3\nv0.10.0-beta2\nv0.5.6\nv0.10.0-beta1\nv0.10.0\nv1.8.7\nv0.5.3\nv1.3.6\nv0.10.2\nv1.1.0-rc1\nv0.6.2\nv0.5.0-rc.2\nv1.8.9\nv0.5.0\nv0.9.0-rc9\nv1.8.2\nv0.9.0-rc6\nv1.11.4\nv1.5.5\nv0.9.0-rc7\nv1.1.1\nv0.5.0-rc.5\nv0.8.0-rc.4\nv0.9.6-rc1\nv0.9.0-rc30\nv0.9.5\nv1.8.6\nv1.9.12\nv0.8.0-rc.5\nv0.10.3\nv1.3.1\nv1.0.0-beta1\nv1.5.0\nv1.2.3-gershontest\nv1.3.4\nv0.0.5\nv1.7.2\nv0.12.0\nv0.8.7\nv1.7.1\nv1.4.1\nv0.12.2\nv0.9.0-rc20\nv1.1.3\nv1.7.11\nv1.9.13\nv1.1.2\nv1.6.1-exp-2-tr\nv1.9.1\nv1.6.3\nv0.8.5\nv1.10.0\nv0.9.0-rc16\nv1.7.3\nv0.5.0-rc.1\nv0.9.2-rc1\nv0.4.4\nv0.9.0\nv0.6.5\nv0.4.0\nv1.9.10\nv0.4.1\nv0.9.0-rc28\nv0.5.4\nv0.9.0-rc25\nv0.0.3\nv1.2.0-rc1\nv1.2.1-rc2\nv0.5.1\nv1.7.5\nv1.11.0\nv0.8.4\nv0.0.4\nv1.5.2\nv0.9.4-rc1\nv1.10.2\nv1.9.4\nv1.0.0-rc1\nv1.2.4\nv1.3.2\nv1.2.1\nv1.5.4\nv0.10.1\nv1.9.3\nv1.2.0-rc2\nv0.3.1\nv1.2.1-rc3\nv0.9.6-rc2\nv0.11.0\nv1.1.5\nv0.5.7\nv0.1.0\nv0.9.3-rc1\nv1.4.0\nv0.7.1\nv0.9.5-rc1\nv0.0.2\nv1.1.4\nv0.9.1\nv1.2.0\nv0.6.3\nv0.9.6\nv1.6.1-exp-tr\nv1.10.4\nv1.9.9\nv1.2.1-rc1\nv1.3.9\nv1.9.11\nv1.2.1-rc5\nv1.2.2\nv0.0.8\nv0.9.0-rc11\nv0.0.9\nv0.10.0-rc1\nv0.9.3-rc2\nv1.6.1\nv0.9.0-rc17\nv1.7.7\nv1.10.1\nv0.13.0-rc2\nv1.5.3\nv0.8.2\nv0.11.1\nv0.9.0-rc32\nv0.5.10\nv1.10.5\nv1.3.5\nv0.13.0\nv1.9.2\nv0.5.9\nv1.6.6\nv0.8.0-rc.1\nv1.7.10\nv1.3.8\nv0.8.6\nv0.9.0-rc5\nv0.9.0-rc8\nv1.9.5\nv0.13.0-rc1\nv1.8.1\nv1.10.3\nv1.6.2\nv1.7.4\nv1.0.1\nv0.9.0-rc2\nv1.11.5\nv0.9.3-rc3\nv1.1.0\nv1.0.0-rc2\nv0.6.4\nv1.11.1\nv0.0.6\nv0.0.7\nv1.8.4\nv0.2.0\nv1.3.7\nv0.9.5-rc2\nv1.0.0-beta2\nv0.8.0-rc.3\nv0.4.2\nv0.9.0-rc15\nv0.8.9\nv0.8.4-rc.1\nv0.9.1-rc1\nv0.3.2\nv0.12.1\nv0.9.4\nv1.7.6\nv0.5.12\nv0.9.0-rc26\n",
+		"status_code": 200
+	},
+	"github.com/influxdata/influxdb/@v/v0.3.2.mod": {
+		"body": "module github.com/influxdata/influxdb\n",
+		"status_code": 200
+	},
+	"github.com/influxdata/influxdb/@v/v1.7.6.mod": {
+		"body": "module github.com/influxdata/influxdb\n",
+		"status_code": 200
+	}
+}
\ No newline at end of file