From 2a668533a28013f1a4fba7e6a547449c2bb9fb6f Mon Sep 17 00:00:00 2001 From: Vinu K Date: Thu, 4 Jun 2026 17:40:44 +0530 Subject: [PATCH] feat: Add ReplaceModule and Dir inside UsedImports - ReplaceVersion does not give complete info. Sometime the module may change - The UsedImports field show the directory wherein the vulnerability is present Signed-off-by: Vinu K --- cmd/cg/main.go | 6 ++ pkg/cmd/cg/scanner.go | 143 +++++++++++++++----------- pkg/cmd/cg/scanner_test.go | 203 +++++++++++++++++++++++++++++++++++++ pkg/cmd/cg/types.go | 2 + 4 files changed, 296 insertions(+), 58 deletions(-) create mode 100644 pkg/cmd/cg/scanner_test.go diff --git a/cmd/cg/main.go b/cmd/cg/main.go index 29c4c93..499f993 100644 --- a/cmd/cg/main.go +++ b/cmd/cg/main.go @@ -250,12 +250,16 @@ func main() { if entry.CurrentVersion == "" { entry.CurrentVersion = symbols.CurrentVersion } + if entry.ReplaceModule == "" { + entry.ReplaceModule = symbols.ReplaceModule + } if entry.ReplaceVersion == "" { entry.ReplaceVersion = symbols.ReplaceVersion } if entry.FixCommands == nil { entry.FixCommands = symbols.FixCommands } + entry.Dir = append(entry.Dir, symbols.Dir...) result.Mu.Lock() mergedImports[pkg] = entry @@ -289,6 +293,8 @@ func main() { sort.Strings(deduped) details.Symbols = deduped } + details.Dir = common.UniqueStrings(details.Dir) + sort.Strings(details.Dir) mergedImports[pkg] = details } diff --git a/pkg/cmd/cg/scanner.go b/pkg/cmd/cg/scanner.go index 426f7b7..c4ca129 100644 --- a/pkg/cmd/cg/scanner.go +++ b/pkg/cmd/cg/scanner.go @@ -254,10 +254,72 @@ func Worker(jobs <-chan Job, results chan<- *Result, wg *sync.WaitGroup, result } } +type VulnerabilityResult struct { + DirVulnerable bool + Status string // "true", "false", or "unknown" + NeedsReplaceFix bool +} + +// checkDirVulnerability determines whether a directory is vulnerable based on +// version comparisons. It returns the vulnerability status and whether a +// replace-directive fix is needed. +func checkDirVulnerability(curVer, repVer, fv string, used, unknown, isStdlib bool, goToolchainVersion string, fixVer []string) VulnerabilityResult { + vr := VulnerabilityResult{Status: "false"} + + if used { + compareVer := curVer + if repVer != "" { + compareVer = repVer + } + + if !isStdlib { + if semver.Compare(compareVer, fv) < 0 { + vr.Status = "true" + vr.DirVulnerable = true + } + } else { + if len(fixVer) > 0 { + isVuln := false + if goToolchainVersion != "" { + appropriateFixVersion := findAppropriateFixVersion(goToolchainVersion, fixVer) + if appropriateFixVersion != "" { + if semver.Compare(goToolchainVersion, appropriateFixVersion) < 0 { + isVuln = true + } + } else { + isVuln = true + } + } + + if goToolchainVersion == "" { + vr.Status = "unknown" + vr.DirVulnerable = true + } else if isVuln { + vr.Status = "true" + vr.DirVulnerable = true + } + } else { + vr.Status = "unknown" + vr.DirVulnerable = true + } + } + } else if unknown { + vr.Status = "unknown" + vr.DirVulnerable = true + } + + if repVer != "" && semver.Compare(curVer, repVer) <= 0 && semver.Compare(repVer, fv) < 0 { + vr.DirVulnerable = true + vr.NeedsReplaceFix = true + } + + return vr +} + func (j Job) isVulnerable(result *Result) *Result { curVer := getCurrentVersion(j.Package, filepath.Join(result.Directory, j.Dir), result) modPath := getModPath(j.Package, filepath.Join(result.Directory, j.Dir), result) - repVer := getReplaceVersion(modPath, filepath.Join(result.Directory, j.Dir), result) + repPath, repVer := getReplaceVersion(modPath, filepath.Join(result.Directory, j.Dir), result) // Check if fixed version is already set (from manual scan), otherwise fetch it var fixVer []string @@ -305,67 +367,30 @@ func (j Job) isVulnerable(result *Result) *Result { } uentry := result.UsedImports[j.Package] uentry.CurrentVersion = curVer - uentry.ReplaceVersion = repVer - if used { - // Determine the version to compare against (prefer replace version if available) - compareVer := curVer - if repVer != "" { - compareVer = repVer - } + if repVer != "" { + uentry.ReplaceModule = repPath + uentry.ReplaceVersion = repVer + } + goToolchainVersion := "" + if result.AffectedImports[j.Package].Type == "stdlib" { + goToolchainVersion = getGoToolchainVersion(filepath.Join(result.Directory, j.Dir), result) + } - if result.AffectedImports[j.Package].Type != "stdlib" { - // For non-stdlib packages: vulnerable if current/replace version is less than fixed version - if semver.Compare(compareVer, fv) < 0 { - result.IsVulnerable = "true" - } else { - result.IsVulnerable = "false" - } - } else { - // For stdlib packages: compare against the Go version fix - if len(fixVer) > 0 { - // Get the actual Go toolchain version - goToolchainVersion := getGoToolchainVersion(filepath.Join(result.Directory, j.Dir), result) + vr := checkDirVulnerability(curVer, repVer, fv, used, unknown, + result.AffectedImports[j.Package].Type == "stdlib", goToolchainVersion, fixVer) - // Find the appropriate fixed version for the current Go major.minor version - isVulnerable := false - if goToolchainVersion != "" { - appropriateFixVersion := findAppropriateFixVersion(goToolchainVersion, fixVer) - if appropriateFixVersion != "" { - if semver.Compare(goToolchainVersion, appropriateFixVersion) < 0 { - isVulnerable = true - } - } else { - // No appropriate fix version found, assume vulnerable - isVulnerable = true - } - } - - if goToolchainVersion == "" { - result.IsVulnerable = "unknown" - } else if isVulnerable { - result.IsVulnerable = "true" - } else { - result.IsVulnerable = "false" - } - } else { - result.IsVulnerable = "unknown" - } - } - } else if unknown { - result.IsVulnerable = "unknown" - } else { - result.IsVulnerable = "false" + result.IsVulnerable = vr.Status + if vr.DirVulnerable { + uentry.Dir = append(uentry.Dir, j.Dir) } - if repVer != "" && semver.Compare(curVer, repVer) <= 0 { + if vr.NeedsReplaceFix { uentry.FixCommands = []string{ fmt.Sprintf("go mod edit -replace=%s=%s@%s", modPath, modPath, fv), "go mod tidy", "go mod vendor", } - } else if result.IsVulnerable == "true" { + } else if vr.Status == "true" { if result.AffectedImports[j.Package].Type == "stdlib" { - // For stdlib packages, select the appropriate Go version to upgrade to - goToolchainVersion := getGoToolchainVersion(filepath.Join(result.Directory, j.Dir), result) selectedFixVersion := selectFixVersionForCurrentGoVersion(goToolchainVersion, fixVer) uentry.FixCommands = []string{ fmt.Sprintf("go mod edit -go=%s", selectedFixVersion), @@ -1120,31 +1145,31 @@ func getGoToolchainVersion(dir string, result *Result) string { return "" } -func getReplaceVersion(pkg string, dir string, result *Result) string { +func getReplaceVersion(pkg string, dir string, result *Result) (string, string) { cmd := "go" args := []string{"mod", "edit", "-json"} out, err := cli.RunCommandStdout(dir, cmd, args...) if err != nil { errMsg := fmt.Sprintf("Failed to run %s %s in %s: %s", cmd, strings.Join(args, " "), dir, strings.TrimSpace(string(out))) result.Errors = append(result.Errors, errMsg) - return "" + return "", "" } var goModEdit GoModEdit err = json.Unmarshal(out, &goModEdit) if err != nil { - return "" + return "", "" } for _, r := range goModEdit.Replace { if r.Old.Path == pkg { if r.New.Version != "" { - return r.New.Version + return r.New.Path, r.New.Version } } } - return "" + return "", "" } func getFixedVersion(id, pkg string, result *Result) []string { @@ -2111,8 +2136,10 @@ func ConvertUsedImports(input map[string]UsedImportsDetails) map[string]interfac result[key] = map[string]interface{}{ "Symbols": details.Symbols, "CurrentVersion": details.CurrentVersion, + "ReplaceModule": details.ReplaceModule, "ReplaceVersion": details.ReplaceVersion, "FixCommands": details.FixCommands, + "Dir": details.Dir, } } return result diff --git a/pkg/cmd/cg/scanner_test.go b/pkg/cmd/cg/scanner_test.go new file mode 100644 index 0000000..7c69952 --- /dev/null +++ b/pkg/cmd/cg/scanner_test.go @@ -0,0 +1,203 @@ +package cg + +import "testing" + +func TestCheckDirVulnerability(t *testing.T) { + tests := []struct { + name string + curVer string + repVer string + fv string + used bool + unknown bool + isStdlib bool + goToolchainVersion string + fixVer []string + wantDirVuln bool + wantStatus string + wantReplaceFix bool + }{ + // --- Non-stdlib, symbol used --- + { + name: "non-stdlib used, current below fix, no replace", + curVer: "v0.23.0", + fv: "v0.33.0", + used: true, + wantStatus: "true", wantDirVuln: true, + }, + { + name: "non-stdlib used, current equals fix, no replace", + curVer: "v0.33.0", + fv: "v0.33.0", + used: true, + wantStatus: "false", wantDirVuln: false, + }, + { + name: "non-stdlib used, current above fix, no replace", + curVer: "v0.34.0", + fv: "v0.33.0", + used: true, + wantStatus: "false", wantDirVuln: false, + }, + { + name: "non-stdlib used, replace below fix", + curVer: "v0.23.0", + repVer: "v0.24.0", + fv: "v0.33.0", + used: true, + wantStatus: "true", wantDirVuln: true, wantReplaceFix: true, + }, + { + name: "non-stdlib used, replace equals fix", + curVer: "v0.23.0", + repVer: "v0.33.0", + fv: "v0.33.0", + used: true, + wantStatus: "false", wantDirVuln: false, + }, + { + name: "non-stdlib used, replace above fix", + curVer: "v0.23.0", + repVer: "v0.34.0", + fv: "v0.33.0", + used: true, + wantStatus: "false", wantDirVuln: false, + }, + + // --- Non-stdlib, symbol not used --- + { + name: "non-stdlib not used, current below fix", + curVer: "v0.23.0", + fv: "v0.33.0", + used: false, + wantStatus: "false", wantDirVuln: false, + }, + { + name: "non-stdlib not used, replace below fix", + curVer: "v0.23.0", + repVer: "v0.24.0", + fv: "v0.33.0", + used: false, + wantStatus: "false", wantDirVuln: true, wantReplaceFix: true, + }, + + // --- Unknown reachability --- + { + name: "unknown reachability", + curVer: "v0.23.0", + fv: "v0.33.0", + unknown: true, + wantStatus: "unknown", wantDirVuln: true, + }, + { + name: "unknown reachability with replace below fix", + curVer: "v0.23.0", + repVer: "v0.24.0", + fv: "v0.33.0", + unknown: true, + wantStatus: "unknown", wantDirVuln: true, wantReplaceFix: true, + }, + + // --- Stdlib, symbol used --- + { + name: "stdlib used, toolchain below fix", + curVer: "v1.21.0", + fv: "v1.21.8", + used: true, + isStdlib: true, + goToolchainVersion: "v1.21.4", + fixVer: []string{"1.21.8", "1.22.2"}, + wantStatus: "true", wantDirVuln: true, + }, + { + name: "stdlib used, toolchain at fix", + curVer: "v1.21.0", + fv: "v1.21.8", + used: true, + isStdlib: true, + goToolchainVersion: "v1.21.8", + fixVer: []string{"1.21.8", "1.22.2"}, + wantStatus: "false", wantDirVuln: false, + }, + { + name: "stdlib used, toolchain above fix", + curVer: "v1.21.0", + fv: "v1.21.8", + used: true, + isStdlib: true, + goToolchainVersion: "v1.22.5", + fixVer: []string{"1.21.8", "1.22.2"}, + wantStatus: "false", wantDirVuln: false, + }, + { + name: "stdlib used, no matching fix for branch", + curVer: "v1.20.0", + fv: "v1.21.8", + used: true, + isStdlib: true, + goToolchainVersion: "v1.20.5", + fixVer: []string{"1.21.8", "1.22.2"}, + wantStatus: "true", wantDirVuln: true, + }, + { + name: "stdlib used, empty toolchain version", + curVer: "v1.21.0", + fv: "v1.21.8", + used: true, + isStdlib: true, + fixVer: []string{"1.21.8"}, + wantStatus: "unknown", wantDirVuln: true, + }, + { + name: "stdlib used, no fix versions available", + curVer: "v1.21.0", + fv: "", + used: true, + isStdlib: true, + wantStatus: "unknown", wantDirVuln: true, + }, + + // --- Real-world mod-dir scenarios --- + { + name: "mod-dir root: require v0.23.0, replace v0.24.0, fix v0.33.0", + curVer: "v0.23.0", + repVer: "v0.24.0", + fv: "v0.33.0", + used: true, + wantStatus: "true", wantDirVuln: true, wantReplaceFix: true, + }, + { + name: "mod-dir bar: require v0.33.0, replace v0.24.0, fix v0.33.0", + curVer: "v0.33.0", + repVer: "v0.24.0", + fv: "v0.33.0", + used: true, + wantStatus: "true", wantDirVuln: true, + }, + { + name: "mod-dir foo: require v0.23.0, replace v0.33.0 (fixed), fix v0.33.0", + curVer: "v0.23.0", + repVer: "v0.33.0", + fv: "v0.33.0", + used: true, + wantStatus: "false", wantDirVuln: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vr := checkDirVulnerability(tt.curVer, tt.repVer, tt.fv, + tt.used, tt.unknown, tt.isStdlib, tt.goToolchainVersion, tt.fixVer) + + if vr.DirVulnerable != tt.wantDirVuln { + t.Errorf("DirVulnerable = %v, want %v", vr.DirVulnerable, tt.wantDirVuln) + } + if vr.Status != tt.wantStatus { + t.Errorf("Status = %q, want %q", vr.Status, tt.wantStatus) + } + if vr.NeedsReplaceFix != tt.wantReplaceFix { + t.Errorf("NeedsReplaceFix = %v, want %v", vr.NeedsReplaceFix, tt.wantReplaceFix) + } + }) + } +} diff --git a/pkg/cmd/cg/types.go b/pkg/cmd/cg/types.go index 3e646a7..0e6b248 100644 --- a/pkg/cmd/cg/types.go +++ b/pkg/cmd/cg/types.go @@ -26,8 +26,10 @@ type AffectedImportsDetails struct { type UsedImportsDetails struct { Symbols []string `json:"Symbols,omitempty"` CurrentVersion string `json:"CurrentVersion,omitempty"` + ReplaceModule string `json:"ReplaceModule,omitempty"` ReplaceVersion string `json:"ReplaceVersion,omitempty"` FixCommands []string `json:"FixCommands,omitempty"` + Dir []string `json:"Dir,omitempty"` Paths [][]*callgraph.Node `json:"-"` // For visualization, not serialized }