diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd0496498..d6c499312 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,9 +73,6 @@ jobs: - name: 'Scan Pull Request' package: 'scanpullrequest' - - name: 'Package Updaters' - package: 'packageupdaters' - os: [ ubuntu, windows, macos ] steps: # Configure prerequisites diff --git a/go.mod b/go.mod index 7df51ab36..745ddc336 100644 --- a/go.mod +++ b/go.mod @@ -9,16 +9,14 @@ require ( github.com/go-git/go-git/v5 v5.19.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v45 v45.2.0 - github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540 + github.com/jfrog/build-info-go v1.13.1-0.20260609044040-eaeffe5a20c3 github.com/jfrog/froggit-go v1.22.0 github.com/jfrog/gofrog v1.7.6 - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260601130310-8d52a530da18 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260609101026-df3091b39d06 github.com/jfrog/jfrog-cli-security v1.29.3 github.com/jfrog/jfrog-client-go v1.55.1-0.20260603130552-af1dd449b994 github.com/owenrumney/go-sarif/v3 v3.2.3 github.com/stretchr/testify v1.11.1 - github.com/tidwall/gjson v1.18.0 - github.com/tidwall/sjson v1.2.5 github.com/urfave/cli/v2 v2.27.7 golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3 ) @@ -65,7 +63,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.7.10 // indirect github.com/jfrog/archiver/v3 v3.6.3 // indirect github.com/jfrog/jfrog-apps-config v1.0.1 // indirect - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260603105750-3886c0f01286 // indirect + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260609101705-321f68d15a6d // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -98,8 +96,10 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/urfave/cli v1.22.17 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect @@ -129,6 +129,9 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) +//orto17:move-package-updaters +replace github.com/jfrog/jfrog-cli-security => github.com/orto17/jfrog-cli-security v0.0.0-20260615072539-fd6cdbdfd113 + // replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security dev // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev diff --git a/go.sum b/go.sum index 4ffc53755..942269f44 100644 --- a/go.sum +++ b/go.sum @@ -138,20 +138,18 @@ github.com/jedib0t/go-pretty/v6 v6.7.10 h1:B/2qW2Bkv2L6n14PP8o1kx75kWzHOQ3YTluWz github.com/jedib0t/go-pretty/v6 v6.7.10/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI= github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540 h1:yJjTgSfmsBx9Q6/iiJxXQ/m0KZfFjNx8nNzaRLCM7z4= -github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= +github.com/jfrog/build-info-go v1.13.1-0.20260609044040-eaeffe5a20c3 h1:S4rdZgq2NLGBcyufsn6e0q2Wb++kCBaDoJYxedwyWmw= +github.com/jfrog/build-info-go v1.13.1-0.20260609044040-eaeffe5a20c3/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= github.com/jfrog/froggit-go v1.22.0 h1:eeN5F8sOUo+h2cXkzArAu4nvSdjkDTAZtgqwrct70qg= github.com/jfrog/froggit-go v1.22.0/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260603105750-3886c0f01286 h1:IF9Fyhfd7hilnuHO2AezV3lE9SF2FSxRxs4gfcU3f1U= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260603105750-3886c0f01286/go.mod h1:GQEGVW3wT1XPykXNsEiPQrF8/+01JvDVcGGYb5vqJuE= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260601130310-8d52a530da18 h1:tPv7XscDFAZaijVwMQNb+HmuucUMYQdjuA5frdGzhF0= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260601130310-8d52a530da18/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= -github.com/jfrog/jfrog-cli-security v1.29.3 h1:cIoDn5NkhmrVANUr22H2IVwYjqeFTA+e61lb4qE+8X8= -github.com/jfrog/jfrog-cli-security v1.29.3/go.mod h1:wTdl1sSLyq+TzOPnncxBBhqCKEqF2kp9l86k+Y5E3mM= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260609101705-321f68d15a6d h1:v1XvZ0ByvzoYyjbg94N7kgBi5NwUyq93xer94JXPCyU= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260609101705-321f68d15a6d/go.mod h1:lYZ5Aaty3xx3N30WULc9HGbOXBbKXkHj7rV1FoX5aYo= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260609101026-df3091b39d06 h1:A8hWKHyvqzGXfWmh+8lXv3waAkim4xiucBfGhl7ZOeQ= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260609101026-df3091b39d06/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-client-go v1.55.1-0.20260603130552-af1dd449b994 h1:z1/WjItD4X9z1VkYhzrnbd0NWXp6+0I/LoP7XmsHl4U= github.com/jfrog/jfrog-client-go v1.55.1-0.20260603130552-af1dd449b994/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= @@ -211,6 +209,8 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/orto17/jfrog-cli-security v0.0.0-20260615072539-fd6cdbdfd113 h1:zCRT+pOTTobwHeshACuc/pLy3WbtDfntmJi5EkmlV/8= +github.com/orto17/jfrog-cli-security v0.0.0-20260615072539-fd6cdbdfd113/go.mod h1:Drewq/fwF6HeCLq7NBgt2My+SHnKQ6g9twQpDEgFeUE= github.com/owenrumney/go-sarif/v3 v3.2.3 h1:n6mdX5ugKwCrZInvBsf6WumXmpAe3mbmQXgkXlIq34U= github.com/owenrumney/go-sarif/v3 v3.2.3/go.mod h1:1bV7t8SZg7pX41spaDkEUs8/yEjzk9JapztMoX1XNjg= github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= diff --git a/main_test.go b/main_test.go index a9dda373c..332411f6e 100644 --- a/main_test.go +++ b/main_test.go @@ -17,7 +17,6 @@ var IntegrationTestPackages = []string{ "github.com/jfrog/frogbot/v3", "github.com/jfrog/frogbot/v3/scanrepository", "github.com/jfrog/frogbot/v3/scanpullrequest", - "github.com/jfrog/frogbot/v3/packageupdaters", } func TestUnitTests(t *testing.T) { diff --git a/packageupdaters/commonpackageupdater.go b/packageupdaters/commonpackageupdater.go deleted file mode 100644 index d5b3949bd..000000000 --- a/packageupdaters/commonpackageupdater.go +++ /dev/null @@ -1,298 +0,0 @@ -package packageupdaters - -import ( - "errors" - "fmt" - "io/fs" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/jfrog/gofrog/datastructures" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" - "golang.org/x/exp/slices" - - "github.com/jfrog/frogbot/v3/utils" -) - -// Node -const ( - nodePackageJSONFileName = "package.json" - nodeModulesDirName = "node_modules" - nodeDependenciesSection = "dependencies" - nodeDevDependenciesSection = "devDependencies" - nodeOptionalDependenciesSection = "optionalDependencies" - nodeOverridesSection = "overrides" - nodePackageManagerInstallTimeout = 15 * time.Minute -) - -var nodePackageManifestSections = []string{ - nodeDependenciesSection, - nodeDevDependenciesSection, - nodeOptionalDependenciesSection, - nodeOverridesSection, -} - -// PackageUpdater interface to hold operations on packages -type PackageUpdater interface { - UpdateDependency(details *utils.VulnerabilityDetails) error -} - -func GetCompatiblePackageUpdater(vulnDetails *utils.VulnerabilityDetails, details *utils.ScanDetails) (handler PackageUpdater) { - switch vulnDetails.Technology { - case techutils.Go: - handler = &GoPackageUpdater{} - case techutils.Poetry: - handler = &PythonPackageUpdater{} - case techutils.Pipenv: - handler = &PythonPackageUpdater{} - case techutils.Npm: - handler = &NpmPackageUpdater{} - case techutils.Yarn: - handler = &YarnPackageUpdater{} - case techutils.Pip: - handler = &PythonPackageUpdater{pipRequirementsFile: defaultRequirementFile} - case techutils.Maven: - handler = &MavenPackageUpdater{} - case techutils.Nuget: - handler = &NugetPackageUpdater{} - case techutils.Gradle: - handler = &GradlePackageUpdater{} - case techutils.Pnpm: - handler = &PnpmPackageUpdater{} - case techutils.Conan: - handler = &ConanPackageUpdater{} - default: - handler = &UnsupportedPackageUpdater{} - } - return -} - -// TODO can be deleted if not needed after refactoring all package updaters -type CommonPackageUpdater struct{} - -// evidencePathLooksLikeNpmPackageCoordinate detects scanner evidence paths like "lodash@4.17.19/package.json" (not real paths). Pnpm filters these; npm does not. -func evidencePathLooksLikeNpmPackageCoordinate(evidenceFile string) bool { - dir := filepath.Dir(evidenceFile) - if dir == "." || dir == "" { - return false - } - for _, part := range strings.Split(filepath.ToSlash(dir), "/") { - if part == "" || part == "." { - continue - } - if strings.Contains(part, "@") && !strings.HasPrefix(part, "@") { - return true - } - } - return false -} - -func (cph *CommonPackageUpdater) CollectVulnerabilityDescriptorPaths(vulnDetails *utils.VulnerabilityDetails, namesFilters []string, ignoreFilters []string) []string { - pathsSet := datastructures.MakeSet[string]() - for _, component := range vulnDetails.Components { - for _, evidence := range component.Evidences { - if evidence.File == "" || techutils.IsTechnologyDescriptor(evidence.File) == techutils.NoTech || slices.ContainsFunc(ignoreFilters, func(pattern string) bool { return strings.Contains(evidence.File, pattern) }) { - continue - } - if len(namesFilters) == 0 || slices.Contains(namesFilters, filepath.Base(evidence.File)) { - pathsSet.Add(evidence.File) - } - } - } - return pathsSet.ToSlice() -} - -// BuildPackageDependencyLineRegex builds a regexp for matching a dependency line in a manifest. -func (cph *CommonPackageUpdater) BuildPackageDependencyLineRegex(impactedName, impactedVersion, dependencyLineFormat string) *regexp.Regexp { - regexpFitImpactedName := strings.ToLower(regexp.QuoteMeta(impactedName)) - regexpFitImpactedVersion := strings.ToLower(regexp.QuoteMeta(impactedVersion)) - regexpCompleteFormat := fmt.Sprintf(strings.ToLower(dependencyLineFormat), regexpFitImpactedName, regexpFitImpactedVersion) - return regexp.MustCompile(regexpCompleteFormat) -} - -func escapeJsonPathKey(key string) string { - r := strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?") - return r.Replace(key) -} - -// GetFixedPackageJSONManifest returns manifest bytes with packageName set to newVersion in allowed sections. -func (cph *CommonPackageUpdater) GetFixedPackageJSONManifest(content []byte, packageName, newVersion, descriptorPath string) ([]byte, error) { - updated := false - escapedName := escapeJsonPathKey(packageName) - - for _, section := range nodePackageManifestSections { - path := section + "." + escapedName - if gjson.GetBytes(content, path).Exists() { - var err error - content, err = sjson.SetBytes(content, path, newVersion) - if err != nil { - return nil, fmt.Errorf("failed to set version for '%s' in section '%s': %w", packageName, section, err) - } - updated = true - } - } - - if !updated { - return nil, fmt.Errorf("package '%s' not found in allowed sections [%s] in '%s'", packageName, strings.Join(nodePackageManifestSections, ", "), descriptorPath) - } - return content, nil -} - -// UpdatePackageJSONDescriptor writes the fixed version for packageName to descriptorPath and returns original file bytes for rollback. -func (cph *CommonPackageUpdater) UpdatePackageJSONDescriptor(descriptorPath, packageName, newVersion string) ([]byte, error) { - //#nosec G304 -- descriptorPath comes from vulnerability evidence in the scanned repository. - descriptorContent, err := os.ReadFile(descriptorPath) - if err != nil { - return nil, fmt.Errorf("failed to read file '%s': %w", descriptorPath, err) - } - - backupContent := make([]byte, len(descriptorContent)) - copy(backupContent, descriptorContent) - - updatedContent, err := cph.GetFixedPackageJSONManifest(descriptorContent, packageName, newVersion, descriptorPath) - if err != nil { - return nil, fmt.Errorf("failed to update version in descriptor: %w", err) - } - - //#nosec G306 G703 -- 0644 for checked-out source; path same trusted source as ReadFile above. - if err = os.WriteFile(descriptorPath, updatedContent, 0644); err != nil { - return nil, fmt.Errorf("failed to write updated descriptor '%s': %w", descriptorPath, err) - } - return backupContent, nil -} - -func (cph *CommonPackageUpdater) withDescriptorWorkingDir(descriptorPath, originalWd string, fn func() error) (err error) { - descriptorDir := filepath.Dir(descriptorPath) - if err = os.Chdir(descriptorDir); err != nil { - return fmt.Errorf("failed to change directory to '%s': %w", descriptorDir, err) - } - defer func() { - if chErr := os.Chdir(originalWd); chErr != nil { - err = errors.Join(err, fmt.Errorf("failed to return to original directory: %w", chErr)) - } - }() - return fn() -} - -func (cph *CommonPackageUpdater) buildEnvWithOverrides(overrides map[string]string) []string { - env := make([]string, 0, len(os.Environ())+len(overrides)) - for _, e := range os.Environ() { - key := strings.SplitN(e, "=", 2)[0] - if _, shouldOverride := overrides[key]; !shouldOverride { - env = append(env, e) - } - } - for key, value := range overrides { - env = append(env, fmt.Sprintf("%s=%s", key, value)) - } - return env -} - -// UpdateDependency updates the impacted package to the fixed version -func (cph *CommonPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails, installationCommand string, extraArgs ...string) (err error) { - // Lower the package name to avoid duplicates - impactedPackage := strings.ToLower(vulnDetails.ImpactedDependencyName) - commandArgs := []string{installationCommand} - commandArgs = append(commandArgs, extraArgs...) - versionOperator := vulnDetails.Technology.GetPackageVersionOperator() - fixedPackageArgs := getFixedPackage(impactedPackage, versionOperator, vulnDetails.SuggestedFixedVersion) - commandArgs = append(commandArgs, fixedPackageArgs...) - return runPackageMangerCommand(vulnDetails.Technology.GetExecCommandName(), vulnDetails.Technology.String(), commandArgs) -} - -func runPackageMangerCommand(commandName string, techName string, commandArgs []string) error { - fullCommand := commandName + " " + strings.Join(commandArgs, " ") - log.Debug(fmt.Sprintf("Running '%s'", fullCommand)) - //#nosec G204 -- False positive - the subprocess only runs after the user's approval. - cmd := exec.Command(commandName, commandArgs...) - if commandName == "pnpm" { - cmd.Env = envWithCorepackIntegrityWorkaround(os.Environ()) - } - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("failed to update %s dependency: '%s' command failed: %s\n%s", techName, fullCommand, err.Error(), output) - } - return nil -} - -// envWithCorepackIntegrityWorkaround sets COREPACK_INTEGRITY_KEYS=0 for older Node/Corepack (e.g. corepack#612). -// Also applied after buildEnvWithOverrides for pnpm lockfile regeneration so Corepack-invoked pnpm matches runPackageMangerCommand behavior. -func envWithCorepackIntegrityWorkaround(base []string) []string { - const key = "COREPACK_INTEGRITY_KEYS" - prefix := key + "=" - out := make([]string, 0, len(base)+1) - for _, e := range base { - if !strings.HasPrefix(e, prefix) { - out = append(out, e) - } - } - return append(out, prefix+"0") -} - -// Returns the updated package and version as it should be run in the update command: -// If the package manager expects a single string (example: @) it returns []string{@} -// If the command args suppose to be seperated by spaces (example: -v ) it returns []string{, "-v", } -func getFixedPackage(impactedPackage string, versionOperator string, suggestedFixedVersion string) (fixedPackageArgs []string) { - fixedPackageString := strings.TrimSpace(impactedPackage) + versionOperator + strings.TrimSpace(suggestedFixedVersion) - fixedPackageArgs = strings.Split(fixedPackageString, " ") - return -} - -// Recursively scans the current directory for descriptor files based on the provided list of suffixes, while excluding paths that match the specified exclusion patterns. -// The patternsToExclude must be provided as regexp patterns. For instance, if the pattern ".*node_modules.*" is provided, any paths containing "node_modules" will be excluded from the result. -// Returns a slice of all discovered descriptor files, represented as absolute paths. -func (cph *CommonPackageUpdater) GetAllDescriptorFilesFullPaths(descriptorFilesSuffixes []string, patternsToExclude ...string) (descriptorFilesFullPaths []string, err error) { - if len(descriptorFilesSuffixes) == 0 { - return - } - - var regexpPatternsCompilers []*regexp.Regexp - for _, patternToExclude := range patternsToExclude { - regexpPatternsCompilers = append(regexpPatternsCompilers, regexp.MustCompile(patternToExclude)) - } - - err = filepath.WalkDir(".", func(path string, d fs.DirEntry, innerErr error) error { - if innerErr != nil { - return fmt.Errorf("an error has occurred when attempting to access or traverse the file system: %w", innerErr) - } - - for _, regexpCompiler := range regexpPatternsCompilers { - if match := regexpCompiler.FindString(path); match != "" { - return filepath.SkipDir - } - } - - for _, assetFileSuffix := range descriptorFilesSuffixes { - if strings.HasSuffix(path, assetFileSuffix) { - var absFilePath string - absFilePath, innerErr = filepath.Abs(path) - if innerErr != nil { - return fmt.Errorf("couldn't retrieve file's absolute path for './%s': %w", path, innerErr) - } - descriptorFilesFullPaths = append(descriptorFilesFullPaths, absFilePath) - } - } - return nil - }) - if err != nil { - err = fmt.Errorf("failed to get descriptor files absolute paths: %w", err) - } - return -} - -func BuildPackageWithVersionRegex(impactedName, impactedVersion, dependencyLineFormat string) *regexp.Regexp { - var c CommonPackageUpdater - return c.BuildPackageDependencyLineRegex(impactedName, impactedVersion, dependencyLineFormat) -} - -func GetVulnerabilityLocations(vulnDetails *utils.VulnerabilityDetails, namesFilters []string, ignoreFilters []string) []string { - var c CommonPackageUpdater - return c.CollectVulnerabilityDescriptorPaths(vulnDetails, namesFilters, ignoreFilters) -} diff --git a/packageupdaters/commonpackageupdater_test.go b/packageupdaters/commonpackageupdater_test.go deleted file mode 100644 index 45958ab4f..000000000 --- a/packageupdaters/commonpackageupdater_test.go +++ /dev/null @@ -1,1325 +0,0 @@ -package packageupdaters - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/jfrog/build-info-go/tests" - biutils "github.com/jfrog/build-info-go/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-security/utils/formats" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/stretchr/testify/assert" - - "github.com/jfrog/frogbot/v3/utils" -) - -type dependencyFixTest struct { - vulnDetails *utils.VulnerabilityDetails - scanDetails *utils.ScanDetails - fixSupported bool - errorExpected bool - specificTechVersion string - testDirName string - descriptorsToCheck []string - testcaseInfo string - // For this param give the relative path from the test project root - lockFileToVerifyItsChange string - // Verifies descriptor content is unchanged after error (for rollback testing) - descriptorToVerifyNoChange string -} - -const ( - requirementsFile = "oslo.config>=1.12.1,<1.13\noslo.utils<5.0,>=4.0.0\nparamiko==2.7.2\npasslib<=1.7.4\nprance>=0.9.0\nprompt-toolkit~=1.0.15\npyinotify>0.9.6\nPyJWT>1.7.1\nurllib3 > 1.1.9, < 1.5.*" -) - -type pipPackageRegexTest struct { - packageName string - expectedRequirement string -} - -func TestUpdateDependency(t *testing.T) { - if strings.TrimSuffix(os.Getenv(utils.JFrogUrlEnv), "/") == "" { - t.Skipf("skipping: %s is not set (package updater integration tests run in CI with platform credentials)", utils.JFrogUrlEnv) - } - scanDetails := &utils.ScanDetails{ - ServerDetails: &config.ServerDetails{}, - } - - testCases := [][]dependencyFixTest{ - // Go test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Go, "golang.org/x/crypto", "", "0.0.0-20201216223049-8b5274cf687f", false, "go.mod"), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"go.mod"}, - lockFileToVerifyItsChange: "go.sum", - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Go, "github.com/google/uuid", "", "1.3.0", true, "go.mod"), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"go.mod"}, - lockFileToVerifyItsChange: "go.sum", - }, - { - testcaseInfo: "no-location-evidence", - vulnDetails: createVulnerabilityDetails(techutils.Go, "github.com/google/uuid", "", "1.3.0", true), - scanDetails: scanDetails, - fixSupported: true, - errorExpected: true, - }, - }, - - // Python test cases (includes pip, pipenv, poetry) - { - { - vulnDetails: createVulnerabilityDetails(techutils.Pip, "urllib3", "", "1.25.9", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Poetry, "urllib3", "", "1.25.9", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Pipenv, "urllib3", "", "1.25.9", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Pip, "pyjwt", "", "2.4.0", true, ""), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"requirements.txt"}, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Pip, "Pyjwt", "", "2.4.0", true, ""), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"requirements.txt"}, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Poetry, "pyjwt", "", "2.4.0", true, ""), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"pyproject.toml"}, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Pipenv, "pyjwt", "", "2.4.0", true, ""), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"Pipfile"}, - }, - }, - - // Npm test cases - { - { - // Test project doesn't exist for the testcase - we just check skipping indirect dependency fix - testcaseInfo: "test-skip-fixing-indirect", - vulnDetails: createVulnerabilityDetails(techutils.Npm, "mpath", "0.8.3", "0.8.4", false, "package-lock.json"), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true, "package.json", "package-lock.json"), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"package.json"}, - lockFileToVerifyItsChange: "package-lock.json", - }, - { - testcaseInfo: "no-location-evidence", - vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true), - scanDetails: scanDetails, - fixSupported: true, - errorExpected: true, - }, - { - testcaseInfo: "rollback-on-npm-install-failure", - vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true, "package.json", "package-lock.json"), - scanDetails: scanDetails, - testDirName: "npm-rollback", - fixSupported: true, - errorExpected: true, - descriptorToVerifyNoChange: "package.json", - }, - }, - - // Yarn test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Yarn, "minimist", "", "1.2.6", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Yarn, "minimist", "", "1.2.6", true, ""), - scanDetails: scanDetails, - fixSupported: true, - specificTechVersion: "1", - descriptorsToCheck: []string{"package.json"}, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Yarn, "minimist", "", "1.2.6", true, ""), - scanDetails: scanDetails, - fixSupported: true, - specificTechVersion: "2", - descriptorsToCheck: []string{"package.json"}, - }, - }, - - // Maven test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Maven, "org.springframework:spring-core", "", "4.3.20", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Maven, "commons-io:commons-io", "", "2.7", true, filepath.Join("multi1", "pom.xml")), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{filepath.Join("multi1", "pom.xml")}, - }, - }, - - // NuGet test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Nuget, "snappier", "1.1.0", "1.1.1", false, ""), - scanDetails: scanDetails, - fixSupported: false, - testDirName: "dotnet", - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Nuget, "snappier", "1.1.0", "1.1.1", true, ""), - scanDetails: scanDetails, - fixSupported: true, - testDirName: "dotnet", - descriptorsToCheck: []string{"dotnet.csproj"}, - }, - }, - - // Gradle test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Gradle, "commons-collections:commons-collections", "3.2", "4.13.1", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - testcaseInfo: "unsupported-dynamic-version", - vulnDetails: createVulnerabilityDetails(techutils.Gradle, "commons-collections:commons-collections", "3.+", "3.2.2", true, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - testcaseInfo: "unsupported-latest-version", - vulnDetails: createVulnerabilityDetails(techutils.Gradle, "commons-collections:commons-collections", "latest.release", "3.2.2", true, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - testcaseInfo: "unsupported-range-version", - vulnDetails: createVulnerabilityDetails(techutils.Gradle, "commons-collections:commons-collections", "[3.0, 3.5.6)", "3.2.2", true, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Gradle, "junit:junit", "4.7", "4.13.1", true, ""), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"build.gradle", filepath.Join("innerProjectForTest", "build.gradle.kts")}, - }, - }, - - // Pnpm test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Pnpm, "mpath", "", "0.8.4", false, ""), - scanDetails: scanDetails, - fixSupported: false, - testDirName: "npm", - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Pnpm, "minimist", "1.2.5", "1.2.6", true, "package.json", "package-lock.json"), - scanDetails: scanDetails, - fixSupported: true, - testDirName: "npm", - descriptorsToCheck: []string{"package.json"}, - }, - }, - - // Conan test cases - { - { - vulnDetails: createVulnerabilityDetails(techutils.Conan, "openssl", "3.0.9", "3.0.14", true, ""), - scanDetails: scanDetails, - fixSupported: true, - testDirName: "conan", - descriptorsToCheck: []string{"conanfile.py", "conanfile.txt"}, - }, - { - vulnDetails: createVulnerabilityDetails(techutils.Conan, "openssl", "3.0.9", "3.0.14", false, ""), - scanDetails: scanDetails, - fixSupported: false, - }, - }, - } - - for _, testBatch := range testCases { - for _, test := range testBatch { - packageUpdater := GetCompatiblePackageUpdater(test.vulnDetails, test.scanDetails) - t.Run(getUpdateDependencyTestcaseName(test.vulnDetails.Technology.String()+test.specificTechVersion, test.vulnDetails.IsDirectDependency, test.testcaseInfo), - func(t *testing.T) { - testDataDir := getTestDataDir(t, test.vulnDetails.IsDirectDependency) - testDirName := test.vulnDetails.Technology.String() - if test.testDirName != "" { - testDirName = test.testDirName - } - cleanup := createTempDirAndChdir(t, testDataDir, testDirName+test.specificTechVersion) - defer cleanup() - - var lockFileContentBeforeUpdate []byte - if test.lockFileToVerifyItsChange != "" { - var readErr error - lockFileContentBeforeUpdate, readErr = os.ReadFile(test.lockFileToVerifyItsChange) - assert.NoError(t, readErr, "Failed to read lock file before update") - } - - var descriptorContentBeforeUpdate []byte - if test.descriptorToVerifyNoChange != "" { - var readErr error - descriptorContentBeforeUpdate, readErr = os.ReadFile(test.descriptorToVerifyNoChange) - assert.NoError(t, readErr, "Failed to read descriptor before update") - } - - err := packageUpdater.UpdateDependency(test.vulnDetails) - if !test.fixSupported { - assert.Error(t, err) - assert.IsType(t, &utils.ErrUnsupportedFix{}, err, "Expected unsupported fix error") - return - } - - if test.errorExpected { - assert.Error(t, err) - if test.descriptorToVerifyNoChange != "" { - descriptorContentAfter, readErr := os.ReadFile(test.descriptorToVerifyNoChange) - assert.NoError(t, readErr, "Failed to read descriptor after update") - assert.Equal(t, descriptorContentBeforeUpdate, descriptorContentAfter, "Descriptor should be unchanged after rollback") - } - return - } - - assert.NoError(t, err) - verifyDependencyUpdate(t, test) - - if test.lockFileToVerifyItsChange != "" { - lockFileContentAfter, readErr := os.ReadFile(test.lockFileToVerifyItsChange) - assert.NoError(t, readErr, "Failed to read lock file after update") - assert.NotEqual(t, lockFileContentBeforeUpdate, lockFileContentAfter, "Lock file should have been updated") - } - }) - } - } -} - -func getTestDataDir(t *testing.T, directDependency bool) string { - var projectDir string - if directDependency { - projectDir = "projects" - } else { - projectDir = "indirect-projects" - } - testdataDir, err := filepath.Abs(filepath.Join("..", "testdata", projectDir)) - assert.NoError(t, err) - return testdataDir -} - -func createTempDirAndChdir(t *testing.T, testdataDir string, tech string) func() { - // Create temp technology project - projectPath := filepath.Join(testdataDir, tech) - tmpProjectPath, cleanup := tests.CreateTestProject(t, projectPath) - currDir, err := os.Getwd() - assert.NoError(t, err) - assert.NoError(t, os.Chdir(tmpProjectPath)) - if tech == "go" { - err = removeTxtSuffix("go.mod.txt") - assert.NoError(t, err) - err = removeTxtSuffix("go.sum.txt") - assert.NoError(t, err) - err = removeTxtSuffix("main.go.txt") - assert.NoError(t, err) - } - return func() { - cleanup() - assert.NoError(t, os.Chdir(currDir)) - } -} - -func removeTxtSuffix(txtFileName string) error { - // go.sum.txt >> go.sum - return fileutils.MoveFile(txtFileName, strings.TrimSuffix(txtFileName, ".txt")) -} - -func assertFixVersionInPackageDescriptor(t *testing.T, test dependencyFixTest, packageDescriptors []string) { - for _, packageDescriptorToCheck := range packageDescriptors { - file, err := os.ReadFile(packageDescriptorToCheck) - assert.NoError(t, err) - - assert.Contains(t, string(file), test.vulnDetails.SuggestedFixedVersion) - // Verify that case-sensitive packages in python are lowered - assert.Contains(t, string(file), strings.ToLower(test.vulnDetails.ImpactedDependencyName)) - } -} - -// Verifies the expected dependency update happened and extra check that are unique to selected package managers -func verifyDependencyUpdate(t *testing.T, test dependencyFixTest) { - if len(test.descriptorsToCheck) == 0 { - assert.Fail(t, fmt.Sprintf("Please provide descriptor files to be inspected in the 'descriptorsToCheck' for %s test cases where a fix is supported.", test.vulnDetails.Technology)) - } - - currDir, err := os.Getwd() - assert.NoError(t, err) - - var descriptorsFullPaths []string - for _, descriptorToCheck := range test.descriptorsToCheck { - descriptorsFullPaths = append(descriptorsFullPaths, filepath.Join(currDir, descriptorToCheck)) - } - - if test.vulnDetails.Technology == techutils.Maven { - // In Maven descriptors the dependency's artifact name and group name are split into 2 different lines, therefore we change the ImpactedDependencyName to be the dependency's artifact name only - depArtifactAndGroup := strings.Split(test.vulnDetails.ImpactedDependencyName, ":") - assert.Equal(t, len(depArtifactAndGroup), 2) - test.vulnDetails.ImpactedDependencyName = depArtifactAndGroup[1] - } - assertFixVersionInPackageDescriptor(t, test, descriptorsFullPaths) - -} - -func TestNugetFixVulnerabilityIfExists(t *testing.T) { - if _, err := exec.LookPath("dotnet"); err != nil { - t.Skipf("skipping: dotnet not in PATH: %v", err) - } - var testcases = []struct { - vulnerabilityDetails *utils.VulnerabilityDetails - }{ - // Basic check - { - vulnerabilityDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "1.1.1", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: techutils.Nuget, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "snappier", ImpactedDependencyVersion: "1.1.0"}}}, - }, - // This testcase checks a fix with a vulnerability that has '.' in the dependency's group and name + more complex version, including letters, to check that the regexp captures them correctly - { - vulnerabilityDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "7.0.11", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: techutils.Nuget, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "Microsoft.Bcl.AsyncInterfaces", ImpactedDependencyVersion: "8.0.0-rc.1.23419.4"}}}, - }, - } - testRootDir, err := os.Getwd() - assert.NoError(t, err) - - tmpDir, err := os.MkdirTemp("", "") - defer func() { - assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) - }() - assert.NoError(t, err) - assert.NoError(t, biutils.CopyDir(filepath.Join("..", "testdata", "projects", "dotnet"), tmpDir, true, nil)) - assert.NoError(t, os.Chdir(tmpDir)) - defer func() { - assert.NoError(t, os.Chdir(testRootDir)) - }() - - nph := &NugetPackageUpdater{} - - descriptorFiles, err := nph.GetAllDescriptorFilesFullPaths([]string{dotnetAssetsFilesSuffix}) - assert.NoError(t, err) - testedDescriptorFile := descriptorFiles[0] - - for _, testcase := range testcases { - vulnRegexpCompiler := BuildPackageWithVersionRegex(testcase.vulnerabilityDetails.ImpactedDependencyName, testcase.vulnerabilityDetails.ImpactedDependencyVersion, dotnetDependencyRegexpPattern) - var isFileChanged bool - isFileChanged, err = nph.fixVulnerabilityIfExists(testcase.vulnerabilityDetails, testedDescriptorFile, tmpDir, vulnRegexpCompiler) - assert.NoError(t, err) - assert.True(t, isFileChanged) - } - - var fixedFileContent []byte - fixedFileContent, err = os.ReadFile(testedDescriptorFile) - fixedFileContentString := string(fixedFileContent) - - assert.NoError(t, err) - assert.NotContains(t, fixedFileContentString, "") - assert.Contains(t, fixedFileContentString, "") - assert.NotContains(t, fixedFileContentString, "") - assert.Contains(t, fixedFileContentString, "") -} - -func TestGetFixedPackage(t *testing.T) { - var testcases = []struct { - impactedPackage string - versionOperator string - suggestedFixedVersion string - expectedOutput []string - }{ - { - impactedPackage: "snappier", - versionOperator: " -v ", - suggestedFixedVersion: "1.1.1", - expectedOutput: []string{"snappier", "-v", "1.1.1"}, - }, - { - impactedPackage: "json", - versionOperator: "@", - suggestedFixedVersion: "10.0.0", - expectedOutput: []string{"json@10.0.0"}, - }, - } - - for _, test := range testcases { - fixedPackageArgs := getFixedPackage(test.impactedPackage, test.versionOperator, test.suggestedFixedVersion) - assert.Equal(t, test.expectedOutput, fixedPackageArgs) - } -} - -func TestGradleFixVulnerabilityIfExists(t *testing.T) { - var testcases = []struct { - vulnerabilityDetails *utils.VulnerabilityDetails - }{ - // Basic check - { - vulnerabilityDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "4.13.1", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: techutils.Gradle, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "junit:junit", ImpactedDependencyVersion: "4.7"}}}, - }, - // This testcase checks a fix with a vulnerability that has '.' in the dependency's group and name + more complex version, including letters, to check that the regexp captures them correctly - { - vulnerabilityDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "1.9.9", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: techutils.Gradle, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "my.group:my.dot.name", ImpactedDependencyVersion: "1.0.0-beta.test"}}}, - }, - } - - currDir, err := os.Getwd() - assert.NoError(t, err) - - tmpDir, err := os.MkdirTemp("", "") - defer func() { - assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) - }() - assert.NoError(t, err) - assert.NoError(t, biutils.CopyDir(filepath.Join("..", "testdata", "projects", "gradle"), tmpDir, true, nil)) - assert.NoError(t, os.Chdir(tmpDir)) - defer func() { - assert.NoError(t, os.Chdir(currDir)) - }() - - gph := GradlePackageUpdater{} - - descriptorFiles, err := gph.GetAllDescriptorFilesFullPaths([]string{groovyDescriptorFileSuffix, kotlinDescriptorFileSuffix}) - assert.NoError(t, err) - - for _, descriptorFile := range descriptorFiles { - for _, testcase := range testcases { - var isFileChanged bool - isFileChanged, err = gph.fixVulnerabilityIfExists(descriptorFile, testcase.vulnerabilityDetails) - assert.NoError(t, err) - assert.True(t, isFileChanged) - } - compareFixedFileToComparisonFile(t, descriptorFile) - } - -} - -func compareFixedFileToComparisonFile(t *testing.T, descriptorFileAbsPath string) { - var compareFilePath string - if strings.HasSuffix(descriptorFileAbsPath, groovyDescriptorFileSuffix) { - curDirPath := strings.TrimSuffix(descriptorFileAbsPath, groovyDescriptorFileSuffix) - compareFilePath = filepath.Join(curDirPath, "fixedBuildGradleForCompare.txt") - } else { - curDirPath := strings.TrimSuffix(descriptorFileAbsPath, kotlinDescriptorFileSuffix) - compareFilePath = filepath.Join(curDirPath, "fixedBuildGradleKtsForCompare.txt") - } - - expectedFileContent, err := os.ReadFile(descriptorFileAbsPath) - assert.NoError(t, err) - - fixedFileContent, err := os.ReadFile(compareFilePath) - assert.NoError(t, err) - - assert.ElementsMatch(t, expectedFileContent, fixedFileContent) -} - -func TestGradleIsVersionSupportedForFix(t *testing.T) { - var testcases = []struct { - impactedVersion string - expectedResult bool - }{ - { - impactedVersion: "10.+", - expectedResult: false, - }, - { - impactedVersion: "[10.3, 11.0)", - expectedResult: false, - }, - { - impactedVersion: "(10.4.2, 11.7.8)", - expectedResult: false, - }, - { - impactedVersion: "latest.release", - expectedResult: false, - }, - { - impactedVersion: "5.5", - expectedResult: true, - }, - { - impactedVersion: "9.0.13-beta", - expectedResult: true, - }, - } - - for _, testcase := range testcases { - assert.Equal(t, testcase.expectedResult, isVersionSupportedForFix(testcase.impactedVersion)) - } -} - -func TestGetAllDescriptorFilesFullPaths(t *testing.T) { - var testcases = []struct { - testProjectRepo string - suffixesToSearch []string - expectedResultSuffixes []string - patternsToExclude []string - }{ - { - testProjectRepo: "dotnet", - suffixesToSearch: []string{dotnetAssetsFilesSuffix}, - expectedResultSuffixes: []string{"dotnet.csproj"}, - }, - { - testProjectRepo: "gradle", - suffixesToSearch: []string{groovyDescriptorFileSuffix, kotlinDescriptorFileSuffix}, - expectedResultSuffixes: []string{filepath.Join("innerProjectForTest", "build.gradle.kts"), "build.gradle"}, - }, - // This test case verifies that paths containing excluded patterns are omitted from the output - { - testProjectRepo: "gradle", - suffixesToSearch: []string{groovyDescriptorFileSuffix, kotlinDescriptorFileSuffix}, - expectedResultSuffixes: []string{"build.gradle"}, - patternsToExclude: []string{".*innerProjectForTest.*"}, - }, - } - - currDir, outerErr := os.Getwd() - assert.NoError(t, outerErr) - - for _, testcase := range testcases { - tmpDir, err := os.MkdirTemp("", "") - assert.NoError(t, err) - assert.NoError(t, biutils.CopyDir(filepath.Join("..", "testdata", "projects", testcase.testProjectRepo), tmpDir, true, nil)) - assert.NoError(t, os.Chdir(tmpDir)) - - finalDirPath, err := os.Getwd() - assert.NoError(t, err) - - var expectedResults []string - for _, suffix := range testcase.expectedResultSuffixes { - expectedResults = append(expectedResults, filepath.Join(finalDirPath, suffix)) - } - - var cph CommonPackageUpdater - descriptorFilesFullPaths, err := cph.GetAllDescriptorFilesFullPaths(testcase.suffixesToSearch, testcase.patternsToExclude...) - assert.NoError(t, err) - assert.ElementsMatch(t, expectedResults, descriptorFilesFullPaths) - - assert.NoError(t, os.Chdir(currDir)) - assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) - } -} - -func TestGetVulnerabilityLocations(t *testing.T) { - testcases := []struct { - name string - vulnDetails *utils.VulnerabilityDetails - namesFilters []string - ignoreFilters []string - expectedPaths []string - }{ - { - name: "single component with descriptor evidence", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "/repo/package.json"}}}, - }, - }, - }, - }, - expectedPaths: []string{"/repo/package.json"}, - }, - { - name: "multiple components with same evidence - deduplicated", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "/repo/package.json"}}}, - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "/repo/package.json"}}}, - }, - }, - }, - }, - expectedPaths: []string{"/repo/package.json"}, - }, - { - name: "multiple components with different descriptor locations", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "/repo/app1/package.json"}}}, - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "/repo/app2/package.json"}}}, - }, - }, - }, - }, - expectedPaths: []string{"/repo/app1/package.json", "/repo/app2/package.json"}, - }, - { - name: "component with empty evidences", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{}}, - }, - }, - }, - }, - expectedPaths: []string{}, - }, - { - name: "component with empty file path in evidence", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: ""}}}, - }, - }, - }, - }, - expectedPaths: []string{}, - }, - { - name: "no components", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{}, - }, - }, - }, - expectedPaths: []string{}, - }, - { - name: "non-descriptor evidences are filtered out", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "package-lock.json"}, - {File: "package.json"}, - }}, - }, - }, - }, - }, - expectedPaths: []string{"package.json"}, - }, - { - name: "filter by basename - match", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "package.json"}}}, - {Name: "lodash", Version: "4.17.0", Evidences: []formats.Location{{File: "go.mod"}}}, - }, - }, - }, - }, - namesFilters: []string{"package.json"}, - expectedPaths: []string{"package.json"}, - }, - { - name: "filter by basename - no match", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "package.json"}}}, - }, - }, - }, - }, - namesFilters: []string{"go.mod"}, - expectedPaths: []string{}, - }, - { - name: "filter by basename - full path matched by basename", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "/repo/apps/frontend/package.json"}}}, - {Name: "lodash", Version: "4.17.0", Evidences: []formats.Location{{File: "/repo/go.mod"}}}, - }, - }, - }, - }, - namesFilters: []string{"package.json"}, - expectedPaths: []string{"/repo/apps/frontend/package.json"}, - }, - { - name: "filter with multiple allowed names", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{{File: "package.json"}}}, - {Name: "lodash", Version: "4.17.0", Evidences: []formats.Location{{File: "go.mod"}}}, - {Name: "axios", Version: "0.21.0", Evidences: []formats.Location{{File: "pyproject.toml"}}}, - }, - }, - }, - }, - namesFilters: []string{"package.json", "go.mod"}, - expectedPaths: []string{"package.json", "go.mod"}, - }, - { - name: "empty filter returns all descriptor locations", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "package-lock.json"}, - {File: "package.json"}, - }}, - }, - }, - }, - }, - namesFilters: []string{}, - expectedPaths: []string{"package.json"}, - }, - { - name: "nil filter returns all descriptor locations", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "package-lock.json"}, - {File: "package.json"}, - }}, - }, - }, - }, - }, - namesFilters: nil, - expectedPaths: []string{"package.json"}, - }, - { - name: "multiple evidences per component - descriptors collected from all", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "/repo/app1/package.json"}, - {File: "/repo/app1/package-lock.json"}, - {File: "/repo/app2/package.json"}, - }}, - }, - }, - }, - }, - expectedPaths: []string{"/repo/app1/package.json", "/repo/app2/package.json"}, - }, - { - name: "ignoreFilters excludes paths containing pattern", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "package.json"}, - {File: "node_modules/minimist/package.json"}, - {File: "libs/node_modules/foo/package.json"}, - }}, - }, - }, - }, - }, - ignoreFilters: []string{"node_modules"}, - expectedPaths: []string{"package.json"}, - }, - { - name: "ignoreFilters with multiple patterns", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "package.json"}, - {File: "node_modules/minimist/package.json"}, - {File: "vendor/something/package.json"}, - }}, - }, - }, - }, - }, - ignoreFilters: []string{"node_modules", "vendor"}, - expectedPaths: []string{"package.json"}, - }, - { - name: "ignoreFilters nil does not filter", - vulnDetails: &utils.VulnerabilityDetails{ - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - {Name: "minimist", Version: "1.2.5", Evidences: []formats.Location{ - {File: "package.json"}, - {File: "sub/package.json"}, - }}, - }, - }, - }, - }, - ignoreFilters: nil, - expectedPaths: []string{"package.json", "sub/package.json"}, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - result := GetVulnerabilityLocations(tc.vulnDetails, tc.namesFilters, tc.ignoreFilters) - assert.ElementsMatch(t, tc.expectedPaths, result) - }) - } -} - -func TestEnvWithCorepackIntegrityWorkaround(t *testing.T) { - t.Parallel() - base := []string{"FOO=1", "COREPACK_INTEGRITY_KEYS=old-value", "BAR=2"} - out := envWithCorepackIntegrityWorkaround(base) - var foo, bar, corepack int - for _, e := range out { - switch { - case e == "FOO=1": - foo++ - case e == "BAR=2": - bar++ - case strings.HasPrefix(e, "COREPACK_INTEGRITY_KEYS="): - corepack++ - assert.Equal(t, "COREPACK_INTEGRITY_KEYS=0", e) - } - } - assert.Equal(t, 1, foo, "FOO should appear once") - assert.Equal(t, 1, bar, "BAR should appear once") - assert.Equal(t, 1, corepack, "COREPACK_INTEGRITY_KEYS should appear exactly once with value 0") -} - -func TestGetVulnerabilityRegexCompiler(t *testing.T) { - // Sample format patterns from different package managers - const ( - npmPattern = `\s*"%s"\s*:\s*"[~^]?%s"` - dotnetPattern = "include=[\\\"|\\']%s[\\\"|\\']\\s*version=[\\\"|\\']%s[\\\"|\\']" - simplePattern = `%s:%s` - ) - - testcases := []struct { - name string - packageName string - packageVer string - formatPattern string - testContent string - shouldMatch bool - }{ - // Basic matching - { - name: "basic npm match", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: `"lodash": "4.17.20"`, - shouldMatch: true, - }, - { - name: "npm with caret prefix", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: `"lodash": "^4.17.20"`, - shouldMatch: true, - }, - { - name: "npm with tilde prefix", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: `"lodash": "~4.17.20"`, - shouldMatch: true, - }, - { - name: "npm version mismatch", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: `"lodash": "4.17.21"`, - shouldMatch: false, - }, - { - name: "npm name mismatch", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: `"underscore": "4.17.20"`, - shouldMatch: false, - }, - - // Case insensitivity - { - name: "case insensitive package name", - packageName: "PyJWT", - packageVer: "2.4.0", - formatPattern: simplePattern, - testContent: `pyjwt:2.4.0`, - shouldMatch: true, - }, - { - name: "case insensitive mixed case", - packageName: "LODASH", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: `"lodash": "4.17.20"`, - shouldMatch: true, - }, - - // Scoped npm packages with @ - { - name: "scoped npm package", - packageName: "@types/node", - packageVer: "18.0.0", - formatPattern: npmPattern, - testContent: `"@types/node": "18.0.0"`, - shouldMatch: true, - }, - { - name: "scoped package with org", - packageName: "@angular/core", - packageVer: "15.0.0", - formatPattern: npmPattern, - testContent: `"@angular/core": "^15.0.0"`, - shouldMatch: true, - }, - - // Regex special characters in package name - should be escaped - { - name: "package name with dot", - packageName: "lodash.merge", - packageVer: "4.6.2", - formatPattern: npmPattern, - testContent: `"lodash.merge": "4.6.2"`, - shouldMatch: true, - }, - { - name: "dot should not match any character", - packageName: "lodash.merge", - packageVer: "4.6.2", - formatPattern: npmPattern, - testContent: `"lodashXmerge": "4.6.2"`, - shouldMatch: false, - }, - { - name: "package name with asterisk", - packageName: "test*package", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test*package:1.0.0`, - shouldMatch: true, - }, - { - name: "asterisk should not match multiple chars", - packageName: "test*package", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `testABCpackage:1.0.0`, - shouldMatch: false, - }, - { - name: "package name with question mark", - packageName: "test?pkg", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test?pkg:1.0.0`, - shouldMatch: true, - }, - { - name: "question mark should not match single char", - packageName: "test?pkg", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `testXpkg:1.0.0`, - shouldMatch: false, - }, - { - name: "package name with brackets", - packageName: "test[pkg]", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test[pkg]:1.0.0`, - shouldMatch: true, - }, - { - name: "package name with parentheses", - packageName: "test(pkg)", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test(pkg):1.0.0`, - shouldMatch: true, - }, - { - name: "package name with curly braces", - packageName: "test{pkg}", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test{pkg}:1.0.0`, - shouldMatch: true, - }, - { - name: "package name with pipe", - packageName: "test|pkg", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test|pkg:1.0.0`, - shouldMatch: true, - }, - { - name: "pipe should not match as OR", - packageName: "test|pkg", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test:1.0.0`, - shouldMatch: false, - }, - { - name: "package name with caret and dollar", - packageName: "^test$", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `^test$:1.0.0`, - shouldMatch: true, - }, - { - name: "package name with backslash", - packageName: `test\pkg`, - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `test\pkg:1.0.0`, - shouldMatch: true, - }, - - // Version with special characters - { - name: "version with plus (build metadata)", - packageName: "mypackage", - packageVer: "1.0.0+build123", - formatPattern: simplePattern, - testContent: `mypackage:1.0.0+build123`, - shouldMatch: true, - }, - { - name: "plus in version should not match one-or-more", - packageName: "mypackage", - packageVer: "1.0.0+", - formatPattern: simplePattern, - testContent: `mypackage:1.0.00000`, - shouldMatch: false, - }, - { - name: "version with dots should match literally", - packageName: "pkg", - packageVer: "1.2.3", - formatPattern: simplePattern, - testContent: `pkg:1.2.3`, - shouldMatch: true, - }, - { - name: "dots should not match any char", - packageName: "pkg", - packageVer: "1.2.3", - formatPattern: simplePattern, - testContent: `pkg:1X2Y3`, - shouldMatch: false, - }, - - // Empty name and version edge cases - { - name: "empty package name matches empty", - packageName: "", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `:1.0.0`, - shouldMatch: true, - }, - { - name: "empty version matches empty", - packageName: "pkg", - packageVer: "", - formatPattern: simplePattern, - testContent: `pkg:`, - shouldMatch: true, - }, - { - name: "both empty", - packageName: "", - packageVer: "", - formatPattern: simplePattern, - testContent: `:`, - shouldMatch: true, - }, - - // Complex realistic scenarios - { - name: "dotnet pattern match", - packageName: "Newtonsoft.Json", - packageVer: "13.0.1", - formatPattern: dotnetPattern, - testContent: `Include="Newtonsoft.Json" Version="13.0.1"`, - shouldMatch: true, - }, - { - name: "dotnet single quotes", - packageName: "Newtonsoft.Json", - packageVer: "13.0.1", - formatPattern: dotnetPattern, - testContent: `Include='Newtonsoft.Json' Version='13.0.1'`, - shouldMatch: true, - }, - - // Whitespace handling - { - name: "npm with extra whitespace", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: ` "lodash" : "4.17.20"`, - shouldMatch: true, - }, - { - name: "npm with tabs", - packageName: "lodash", - packageVer: "4.17.20", - formatPattern: npmPattern, - testContent: "\t\"lodash\"\t:\t\"4.17.20\"", - shouldMatch: true, - }, - - // Unicode characters (less common but possible) - { - name: "package name with unicode", - packageName: "пакет", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `пакет:1.0.0`, - shouldMatch: true, - }, - - // Long package names and versions - { - name: "very long package name", - packageName: "this-is-a-very-long-package-name-that-might-exist-in-real-world", - packageVer: "1.0.0", - formatPattern: simplePattern, - testContent: `this-is-a-very-long-package-name-that-might-exist-in-real-world:1.0.0`, - shouldMatch: true, - }, - { - name: "prerelease version", - packageName: "pkg", - packageVer: "1.0.0-alpha.1", - formatPattern: simplePattern, - testContent: `pkg:1.0.0-alpha.1`, - shouldMatch: true, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - regex := BuildPackageWithVersionRegex(tc.packageName, tc.packageVer, tc.formatPattern) - matches := regex.MatchString(strings.ToLower(tc.testContent)) - assert.Equal(t, tc.shouldMatch, matches, "Pattern: %s, Content: %s", regex.String(), tc.testContent) - }) - } -} - -func getUpdateDependencyTestcaseName(technology string, isDirect bool, extraTestInfo string) string { - testName := technology - if isDirect { - testName += "-direct-dep" - } else { - testName += "-indirect-dep" - } - if extraTestInfo != "" { - testName += "_(" + extraTestInfo + ")" - } - return testName -} - -func createVulnerabilityDetails(technology techutils.Technology, packageName, packageVersion, fixedVersion string, isDirectDependency bool, evidencePaths ...string) *utils.VulnerabilityDetails { - var evidences []formats.Location - for _, path := range evidencePaths { - if path != "" { - evidences = append(evidences, formats.Location{File: path}) - } - } - return &utils.VulnerabilityDetails{ - SuggestedFixedVersion: fixedVersion, - IsDirectDependency: isDirectDependency, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: technology, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: packageName, - ImpactedDependencyVersion: packageVersion, - Components: []formats.ComponentRow{ - { - Name: packageName, - Version: packageVersion, - Evidences: evidences, - }, - }, - }, - }, - } -} diff --git a/packageupdaters/conanpackageupdater.go b/packageupdaters/conanpackageupdater.go deleted file mode 100644 index 3337d35f0..000000000 --- a/packageupdaters/conanpackageupdater.go +++ /dev/null @@ -1,78 +0,0 @@ -package packageupdaters - -import ( - "fmt" - "os" - "strings" - - "github.com/jfrog/frogbot/v3/utils" - "github.com/jfrog/jfrog-client-go/utils/log" -) - -const ( - conanFileTxt = "conanfile.txt" - conanFilePy = "conanfile.py" -) - -type ConanPackageUpdater struct { - CommonPackageUpdater -} - -func (conan *ConanPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return conan.updateDirectDependency(vulnDetails) - } else { - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } - } -} - -func (conan *ConanPackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - conanDescriptors, err := conan.CommonPackageUpdater.GetAllDescriptorFilesFullPaths([]string{conanFileTxt, conanFilePy}) - if err != nil { - err = fmt.Errorf("failed while searching for Conan descriptor files in project: %s", err.Error()) - return - } - isAnyDescriptorFileChanged := false - for _, descriptor := range conanDescriptors { - var isFileChanged bool - isFileChanged, err = conan.updateConanFile(descriptor, vulnDetails) - if err != nil { - return - } - isAnyDescriptorFileChanged = isAnyDescriptorFileChanged || isFileChanged - } - if !isAnyDescriptorFileChanged { - err = fmt.Errorf("impacted package '%s' was not found or could not be fixed in all descriptor files", vulnDetails.ImpactedDependencyName) - } else { - log.Info("Requirements file was updated with a suggested fix version, but no installation was performed. " + - "In order to update the dependencies, please run 'conan install' command") - } - return -} - -func (conan *ConanPackageUpdater) updateConanFile(conanFilePath string, vulnDetails *utils.VulnerabilityDetails) (isFileChanged bool, err error) { - //#nosec G304 -- descriptor path from scan workflow. - data, err := os.ReadFile(conanFilePath) - if err != nil { - return false, fmt.Errorf("an error occurred while attempting to read the requirements file '%s': %s", conanFilePath, err.Error()) - } - currentFile := string(data) - fixedPackage := vulnDetails.ImpactedDependencyName + "/" + vulnDetails.SuggestedFixedVersion - impactedDependency := vulnDetails.ImpactedDependencyName + "/" + vulnDetails.ImpactedDependencyVersion - fixedFile := strings.Replace(currentFile, impactedDependency, strings.ToLower(fixedPackage), 1) - - if fixedFile == currentFile { - log.Debug(fmt.Sprintf("impacted dependency '%s' not found in descriptor '%s', moving to the next descriptor if exists...", impactedDependency, conanFilePath)) - return false, nil - } - //#nosec G703 -- False positive - the path is determined by internal file scanning, not user input, and was already validated by the preceding Stat call. - if err = os.WriteFile(conanFilePath, []byte(fixedFile), 0600); err != nil { - err = fmt.Errorf("an error occured while writing the fixed version of %s to the requirements file '%s': %s", vulnDetails.ImpactedDependencyName, conanFilePath, err.Error()) - } - isFileChanged = true - return -} diff --git a/packageupdaters/gopackageupdater.go b/packageupdaters/gopackageupdater.go deleted file mode 100644 index 3f4c63a3c..000000000 --- a/packageupdaters/gopackageupdater.go +++ /dev/null @@ -1,229 +0,0 @@ -package packageupdaters - -import ( - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/jfrog/frogbot/v3/utils" - "github.com/jfrog/jfrog-client-go/utils/log" -) - -const ( - goFlagModEditEnv = "GOFLAGS=-mod=mod" - goWorkOffEnv = "GOWORK=off" - goModFileName = "go.mod" - goSumFileName = "go.sum" - goVendorDirName = "vendor" - goTidyContinueOnError = "-e" -) - -type GoPackageUpdater struct{} - -type goModuleBackup struct { - goModPath string - goModContent []byte - goSumPath string - goSumContent []byte -} - -func (gpu *GoPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - descriptorPaths := GetVulnerabilityLocations(vulnDetails, []string{goModFileName}, []string{goVendorDirName}) - if len(descriptorPaths) == 0 { - return fmt.Errorf("no descriptor evidence was found for package %s", vulnDetails.ImpactedDependencyName) - } - - originalWd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - - env := gpu.buildGoCommandEnv() - - var failingDescriptors []string - for _, descriptorPath := range descriptorPaths { - if fixErr := gpu.fixVulnerabilityAndTidy(vulnDetails, descriptorPath, originalWd, env); fixErr != nil { - failedFixErrorMsg := fmt.Errorf("failed to fix '%s' in descriptor '%s': %w", vulnDetails.ImpactedDependencyName, descriptorPath, fixErr) - log.Warn(failedFixErrorMsg.Error()) - err = errors.Join(err, failedFixErrorMsg) - failingDescriptors = append(failingDescriptors, descriptorPath) - } - } - if err != nil { - return fmt.Errorf("encountered errors while fixing '%s' vulnerability in descriptors [%s]: %w", vulnDetails.ImpactedDependencyName, strings.Join(failingDescriptors, ", "), err) - } - - return nil -} - -func (gpu *GoPackageUpdater) fixVulnerabilityAndTidy(vulnDetails *utils.VulnerabilityDetails, descriptorPath, originalWd string, env []string) (err error) { - backup, backupErr := gpu.backupModuleFiles(descriptorPath) - if backupErr != nil { - return backupErr - } - - descriptorDir := filepath.Dir(descriptorPath) - if err = os.Chdir(descriptorDir); err != nil { - return fmt.Errorf("failed to change directory to '%s': %w", descriptorDir, err) - } - defer func() { - if chErr := os.Chdir(originalWd); chErr != nil { - err = errors.Join(err, fmt.Errorf("failed to return to original directory: %w", chErr)) - } - }() - - if err = gpu.updateDependency(vulnDetails, env); err != nil { - log.Warn(fmt.Sprintf("Failed to update '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) - if rollbackErr := gpu.restoreModuleFiles(backup); rollbackErr != nil { - return fmt.Errorf("failed to rollback module files after go get failure: %w (original error: %v)", rollbackErr, err) - } - return err - } - - lockFileTracked, checkErr := utils.IsFileTrackedByGit(backup.goSumPath, originalWd) - if checkErr != nil { - log.Debug(fmt.Sprintf("Failed to check if lock file is tracked in git: %s. Proceeding with lock file regeneration.", checkErr.Error())) - lockFileTracked = true - } - - if !lockFileTracked { - log.Debug(fmt.Sprintf("Lock file '%s' is not tracked in git, skipping lock file regeneration", backup.goSumPath)) - return nil - } - - if err = gpu.tidyLockFiles(descriptorDir, env); err != nil { - log.Warn(fmt.Sprintf("Failed to tidy module files after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) - if rollbackErr := gpu.restoreModuleFiles(backup); rollbackErr != nil { - return fmt.Errorf("failed to rollback module files after tidy failure: %w (original error: %v)", rollbackErr, err) - } - return err - } - - log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s' in descriptor '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion, descriptorPath)) - return nil -} - -func (gpu *GoPackageUpdater) buildGoCommandEnv() []string { - return append(os.Environ(), goFlagModEditEnv, goWorkOffEnv) -} - -func (gpu *GoPackageUpdater) backupModuleFiles(goModPath string) (*goModuleBackup, error) { - //#nosec G304 -- go.mod path from scan workflow. - goModContent, err := os.ReadFile(goModPath) - if err != nil { - return nil, fmt.Errorf("failed to read '%s': %w", goModPath, err) - } - - // We assume go.sum resides under the same directory as go.mod - descriptorDir := filepath.Dir(goModPath) - goSumPath := filepath.Join(descriptorDir, goSumFileName) - //#nosec G304 -- go.sum adjacent to go.mod from same scan workflow. - goSumContent, err := os.ReadFile(goSumPath) - if err != nil { - return nil, fmt.Errorf("failed to read '%s': %w", goSumPath, err) - } - - backup := &goModuleBackup{ - goModPath: goModPath, - goModContent: make([]byte, len(goModContent)), - goSumPath: goSumPath, - goSumContent: make([]byte, len(goSumContent)), - } - copy(backup.goModContent, goModContent) - copy(backup.goSumContent, goSumContent) - - return backup, nil -} - -func (gpu *GoPackageUpdater) restoreModuleFiles(backup *goModuleBackup) error { - //#nosec G306 -- 0644 for checked-out module files in workspace. - if err := os.WriteFile(backup.goModPath, backup.goModContent, 0644); err != nil { - return fmt.Errorf("failed to restore '%s': %w", backup.goModPath, err) - } - //#nosec G306 -- 0644 for checked-out module files in workspace. - if err := os.WriteFile(backup.goSumPath, backup.goSumContent, 0644); err != nil { - return fmt.Errorf("failed to restore '%s': %w", backup.goSumPath, err) - } - log.Debug(fmt.Sprintf("Successfully rolled back '%s' and '%s' to original state", backup.goModPath, backup.goSumPath)) - return nil -} - -func (gpu *GoPackageUpdater) updateDependency(vulnDetails *utils.VulnerabilityDetails, env []string) error { - impactedPackage := strings.ToLower(vulnDetails.ImpactedDependencyName) - fixedVersion := strings.TrimSpace(vulnDetails.SuggestedFixedVersion) - - if !strings.HasPrefix(fixedVersion, "v") { - fixedVersion = "v" + fixedVersion - } - fixedPackage := strings.TrimSpace(impactedPackage) + "@" + fixedVersion - - //#nosec G204 -- runs only after user approval; arguments from vulnerability metadata. - cmd := exec.Command("go", "get", fixedPackage) - cmd.Env = env - log.Debug(fmt.Sprintf("Running 'go get %s'", fixedPackage)) - - output, err := cmd.CombinedOutput() - if len(output) > 0 { - log.Debug(fmt.Sprintf("go get output:\n%s", string(output))) - } - - if err != nil { - return fmt.Errorf("go get failed: %s\n%s", err.Error(), output) - } - return nil -} - -func (gpu *GoPackageUpdater) tidyLockFiles(descriptorDir string, env []string) error { - cmd := exec.Command("go", "mod", "tidy", goTidyContinueOnError) - cmd.Env = env - log.Debug("Running 'go mod tidy'") - - //#nosec G204 -- False positive - the subprocess only runs after the user's approval. - output, err := cmd.CombinedOutput() - if len(output) > 0 { - log.Debug(fmt.Sprintf("go mod tidy output:\n%s", string(output))) - } - - if err != nil { - return fmt.Errorf("go mod tidy failed: %s\n%s", err.Error(), output) - } - - if gpu.hasVendorDirectory(descriptorDir) { - if err := gpu.updateVendor(env); err != nil { - return err - } - } - - return nil -} - -func (gpu *GoPackageUpdater) hasVendorDirectory(descriptorDir string) bool { - vendorModulesPath := filepath.Join(descriptorDir, goVendorDirName, "modules.txt") - if _, err := os.Stat(vendorModulesPath); err == nil { - log.Debug(fmt.Sprintf("Detected vendor directory at: %s", vendorModulesPath)) - return true - } - return false -} - -func (gpu *GoPackageUpdater) updateVendor(env []string) error { - vendorCmd := exec.Command("go", "mod", "vendor") - vendorCmd.Env = env - log.Debug("Running 'go mod vendor' to update vendored dependencies") - - //#nosec G204 -- False positive - the subprocess only runs after the user's approval. - vendorOutput, err := vendorCmd.CombinedOutput() - if len(vendorOutput) > 0 { - log.Debug(fmt.Sprintf("go mod vendor output:\n%s", string(vendorOutput))) - } - - if err != nil { - return fmt.Errorf("go mod vendor failed: %s\n%s", err.Error(), vendorOutput) - } - - log.Debug("Successfully updated vendor directory") - return nil -} diff --git a/packageupdaters/gopackageupdater_test.go b/packageupdaters/gopackageupdater_test.go deleted file mode 100644 index 9a51cbeb7..000000000 --- a/packageupdaters/gopackageupdater_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package packageupdaters - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBackupModuleFiles(t *testing.T) { - testcases := []struct { - name string - goModContent []byte - goSumContent []byte - modifiedGoMod []byte - modifiedGoSum []byte - }{ - { - name: "backup and restore after files were modified", - goModContent: []byte("module example.com/test\n\ngo 1.21\n\nrequire github.com/some/pkg v1.0.0\n"), - goSumContent: []byte("github.com/some/pkg v1.0.0 h1:abc123=\ngithub.com/some/pkg v1.0.0/go.mod h1:def456=\n"), - modifiedGoMod: []byte("module example.com/test\n\ngo 1.21\n\nrequire github.com/some/pkg v2.0.0\n"), - modifiedGoSum: []byte("github.com/some/pkg v2.0.0 h1:xyz789=\n"), - }, - { - name: "backup preserves empty go.sum", - goModContent: []byte("module example.com/test\n\ngo 1.21\n"), - goSumContent: []byte(""), - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - tmpDir := t.TempDir() - goModPath := filepath.Join(tmpDir, goModFileName) - goSumPath := filepath.Join(tmpDir, goSumFileName) - assert.NoError(t, os.WriteFile(goModPath, tc.goModContent, 0644)) - assert.NoError(t, os.WriteFile(goSumPath, tc.goSumContent, 0644)) - - gpu := &GoPackageUpdater{} - backup, err := gpu.backupModuleFiles(goModPath) - assert.NoError(t, err) - assert.Equal(t, tc.goModContent, backup.goModContent) - assert.Equal(t, tc.goSumContent, backup.goSumContent) - assert.Equal(t, goModPath, backup.goModPath) - assert.Equal(t, goSumPath, backup.goSumPath) - - if tc.modifiedGoMod != nil { - assert.NoError(t, os.WriteFile(goModPath, tc.modifiedGoMod, 0644)) - assert.NoError(t, os.WriteFile(goSumPath, tc.modifiedGoSum, 0644)) - - assert.NoError(t, gpu.restoreModuleFiles(backup)) - - restoredGoMod, err := os.ReadFile(goModPath) - assert.NoError(t, err) - assert.Equal(t, tc.goModContent, restoredGoMod) - - restoredGoSum, err := os.ReadFile(goSumPath) - assert.NoError(t, err) - assert.Equal(t, tc.goSumContent, restoredGoSum) - } - }) - } -} - -func TestHasVendorDirectory(t *testing.T) { - testcases := []struct { - name string - setupVendor bool - expectedResult bool - }{ - { - name: "vendor directory with modules.txt", - setupVendor: true, - expectedResult: true, - }, - { - name: "no vendor directory", - setupVendor: false, - expectedResult: false, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - tmpDir := t.TempDir() - - if tc.setupVendor { - vendorDir := filepath.Join(tmpDir, goVendorDirName) - assert.NoError(t, os.MkdirAll(vendorDir, 0755)) - assert.NoError(t, os.WriteFile(filepath.Join(vendorDir, "modules.txt"), []byte("# vendor modules\n"), 0644)) - } - - gpu := &GoPackageUpdater{} - assert.Equal(t, tc.expectedResult, gpu.hasVendorDirectory(tmpDir)) - }) - } -} diff --git a/packageupdaters/gradlepackageupdater.go b/packageupdaters/gradlepackageupdater.go deleted file mode 100644 index 7bec5984a..000000000 --- a/packageupdaters/gradlepackageupdater.go +++ /dev/null @@ -1,159 +0,0 @@ -package packageupdaters - -import ( - "fmt" - "github.com/jfrog/frogbot/v3/utils" - "os" - "regexp" - "strings" -) - -const ( - groovyDescriptorFileSuffix = "build.gradle" - kotlinDescriptorFileSuffix = "build.gradle.kts" - apostrophes = "[\\\"|\\']" - directMapRegexpEntry = "\\s*%s\\s*[:|=]\\s*" - directStringWithVersionFormat = "%s:%s:%s" -) - -// Regexp pattern for "map" format dependencies -// Example: group: "junit", name: "junit", version: "1.0.0" | group = "junit", name = "junit", version = "1.0.0" -var directMapWithVersionRegexp = getMapRegexpEntry("group") + "," + getMapRegexpEntry("name") + "," + getMapRegexpEntry("version") - -var gradleDescriptorsSuffixes = []string{groovyDescriptorFileSuffix, kotlinDescriptorFileSuffix} - -func getMapRegexpEntry(mapEntry string) string { - return fmt.Sprintf(directMapRegexpEntry, mapEntry) + apostrophes + "%s" + apostrophes -} - -type GradlePackageUpdater struct { - CommonPackageUpdater -} - -func (gph *GradlePackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return gph.updateDirectDependency(vulnDetails) - } - - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } -} - -func (gph *GradlePackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - if !isVersionSupportedForFix(vulnDetails.ImpactedDependencyVersion) { - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.UnsupportedForFixVulnerableVersion, - } - } - - // A gradle project may contain several descriptor files in several sub-modules. Each vulnerability may be found in each of the descriptor files. - // Therefore we iterate over every descriptor file for each vulnerability and try to find and fix it. - var descriptorFilesFullPaths []string - descriptorFilesFullPaths, err = gph.GetAllDescriptorFilesFullPaths(gradleDescriptorsSuffixes) - if err != nil { - return - } - - isAnyDescriptorFileChanged := false - for _, descriptorFilePath := range descriptorFilesFullPaths { - var isFileChanged bool - isFileChanged, err = gph.fixVulnerabilityIfExists(descriptorFilePath, vulnDetails) - if err != nil { - return - } - // We use logical OR to save information over all descriptor files whether there is at least one file that has been changed - isAnyDescriptorFileChanged = isAnyDescriptorFileChanged || isFileChanged - } - - if !isAnyDescriptorFileChanged { - err = fmt.Errorf("impacted package '%s' was not found or could not be fixed in all descriptor files", vulnDetails.ImpactedDependencyName) - } - return -} - -// Checks if the impacted version is currently supported for fix -func isVersionSupportedForFix(impactedVersion string) bool { - if strings.Contains(impactedVersion, "+") || - (strings.Contains(impactedVersion, "[") || strings.Contains(impactedVersion, "(")) || - strings.Contains(impactedVersion, "latest.release") { - return false - } - return true -} - -// Fixes all direct occurrences of the given vulnerability in the given descriptor file, if vulnerability occurs -func (gph *GradlePackageUpdater) fixVulnerabilityIfExists(descriptorFilePath string, vulnDetails *utils.VulnerabilityDetails) (isFileChanged bool, err error) { - //#nosec G304 -- descriptor path from scan workflow. - byteFileContent, err := os.ReadFile(descriptorFilePath) - if err != nil { - err = fmt.Errorf("couldn't read file '%s': %s", descriptorFilePath, err.Error()) - return - } - fileContent := string(byteFileContent) - originalFile := fileContent - - depGroup, depName, err := getVulnerabilityGroupAndName(vulnDetails.ImpactedDependencyName) - if err != nil { - return - } - - // Fixing all vulnerable rows given in a string format. For Example: implementation "junit:junit:4.7" - directStringVulnerableRow := fmt.Sprintf(directStringWithVersionFormat, depGroup, depName, vulnDetails.ImpactedDependencyVersion) - directStringFixedRow := fmt.Sprintf(directStringWithVersionFormat, depGroup, depName, vulnDetails.SuggestedFixedVersion) - fileContent = strings.ReplaceAll(fileContent, directStringVulnerableRow, directStringFixedRow) - - // We replace '.' characters to '\\.' since '.' in order to correctly capture '.' character using regexps - regexpAdjustedDepGroup := strings.ReplaceAll(depGroup, ".", "\\.") - regexpAdjustedDepName := strings.ReplaceAll(depName, ".", "\\.") - regexpAdjustedImpactedVersion := strings.ReplaceAll(vulnDetails.ImpactedDependencyVersion, ".", "\\.") - - // Fixing all vulnerable rows given in a map format. For Example: implementation group: "junit", name: "junit", version: "4.7" - mapRegexpForVulnerability := fmt.Sprintf(directMapWithVersionRegexp, regexpAdjustedDepGroup, regexpAdjustedDepName, regexpAdjustedImpactedVersion) - regexpCompiler := regexp.MustCompile(mapRegexpForVulnerability) - if rowsMatches := regexpCompiler.FindAllString(fileContent, -1); rowsMatches != nil { - for _, entry := range rowsMatches { - fixedRow := strings.Replace(entry, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion, 1) - fileContent = strings.ReplaceAll(fileContent, entry, fixedRow) - } - } - - // If there is no changes in the file we finish dealing with the current descriptor file - if fileContent == originalFile { - return - } - isFileChanged = true - - err = writeUpdatedBuildFile(descriptorFilePath, fileContent) - return -} - -// Returns separated 'group' and 'name' for a given vulnerability name. In addition replaces every '.' char into '\\.' since the output will be used for a regexp -func getVulnerabilityGroupAndName(impactedDependencyName string) (depGroup string, depName string, err error) { - seperatedImpactedDepName := strings.Split(impactedDependencyName, ":") - if len(seperatedImpactedDepName) != 2 { - err = fmt.Errorf("unable to parse impacted dependency name '%s'", impactedDependencyName) - return - } - return seperatedImpactedDepName[0], seperatedImpactedDepName[1], err -} - -// Writes the updated content of the descriptor's file into the file -func writeUpdatedBuildFile(filePath string, fileContent string) (err error) { - fileInfo, err := os.Stat(filePath) - if err != nil { - err = fmt.Errorf("couldn't get file info for file '%s': %s", filePath, err.Error()) - return - } - - //#nosec G703 -- False positive - the path is determined by internal file scanning, not user input, and was already validated by the preceding Stat call. - err = os.WriteFile(filePath, []byte(fileContent), fileInfo.Mode()) - if err != nil { - err = fmt.Errorf("couldn't write fixes to file '%s': %q", filePath, err) - } - return -} diff --git a/packageupdaters/mavenpackageupdater.go b/packageupdaters/mavenpackageupdater.go deleted file mode 100644 index 75adc88eb..000000000 --- a/packageupdaters/mavenpackageupdater.go +++ /dev/null @@ -1,201 +0,0 @@ -package packageupdaters - -import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "os" - "regexp" - "strings" - - "github.com/jfrog/jfrog-client-go/utils/log" - - "github.com/jfrog/frogbot/v3/utils" -) - -const ( - mavenDependencySeparator = ":" - propertyPrefix = "${" - propertySuffix = "}" - pomFileName = "pom.xml" -) - -type MavenPackageUpdater struct{} - -type mavenProject struct { - XMLName xml.Name `xml:"project"` - Parent *mavenDep `xml:"parent"` - Properties *mavenProperties `xml:"properties"` - Dependencies []mavenDep `xml:"dependencies>dependency"` - DependencyManagement *mavenDepManagement `xml:"dependencyManagement"` -} - -type mavenProperties struct { - Props []mavenProperty `xml:",any"` -} - -type mavenProperty struct { - XMLName xml.Name - Value string `xml:",chardata"` -} - -type mavenDep struct { - GroupId string `xml:"groupId"` - ArtifactId string `xml:"artifactId"` - Version string `xml:"version"` -} - -type mavenDepManagement struct { - Dependencies []mavenDep `xml:"dependencies>dependency"` -} - -func (m *MavenPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if !vulnDetails.IsDirectDependency { - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } - } - - groupId, artifactId, err := parseDependencyName(vulnDetails.ImpactedDependencyName) - if err != nil { - return err - } - - pomPaths := GetVulnerabilityLocations(vulnDetails, []string{pomFileName}, []string{}) - if len(pomPaths) == 0 { - return fmt.Errorf("no pom.xml locations found for %s - Components array is empty or missing Location data", vulnDetails.ImpactedDependencyName) - } - log.Verbose(fmt.Sprintf("Found vulnerability %s occurrences for component %s in %s", vulnDetails.IssueId, vulnDetails.ImpactedDependencyVersion, strings.Join(pomPaths, ", "))) - - var failingDescriptors []string - for _, pomPath := range pomPaths { - if fixErr := m.updatePomFile(pomPath, groupId, artifactId, vulnDetails.SuggestedFixedVersion); fixErr != nil { - log.Warn(fixErr.Error()) - failedFixErrorMsg := fmt.Errorf("failed to fix '%s' in descriptor '%s': %w", vulnDetails.ImpactedDependencyName, pomPath, fixErr) - err = errors.Join(err, failedFixErrorMsg) - failingDescriptors = append(failingDescriptors, pomPath) - } - log.Debug("Updated successfully " + pomPath) - } - - if err != nil { - return fmt.Errorf("encountered errors while fixing '%s' vulnerability in descriptors [%s]: %w", vulnDetails.ImpactedDependencyName, strings.Join(failingDescriptors, ", "), err) - } - return nil -} - -func (m *MavenPackageUpdater) updatePomFile(pomPath, groupId, artifactId, fixedVersion string) error { - //#nosec G304 -- pomPath from descriptor discovery in the scanned repository. - content, err := os.ReadFile(pomPath) - if err != nil { - return fmt.Errorf("failed to read %s: %w", pomPath, err) - } - - var project mavenProject - if err = xml.Unmarshal(content, &project); err != nil { - return fmt.Errorf("failed to parse %s: %w", pomPath, err) - } - - // Working buffer for the update chain. Each update returns a new slice (e.g. regexp.ReplaceAll), - // so we never mutate the original content. No backup or revert—we only write to the file on success. - currentContent := content - var updatedAny bool - - if updated, c := m.updateInParent(&project, groupId, artifactId, fixedVersion, currentContent); updated { - currentContent = c - updatedAny = true - } - if updated, c := m.updateInDependencies(&project, project.Dependencies, groupId, artifactId, fixedVersion, currentContent); updated { - currentContent = c - updatedAny = true - } - if project.DependencyManagement != nil { - if updated, c := m.updateInDependencies(&project, project.DependencyManagement.Dependencies, groupId, artifactId, fixedVersion, currentContent); updated { - currentContent = c - updatedAny = true - } - } - - if !updatedAny { - return fmt.Errorf("dependency %s not found in %s", toDependencyName(groupId, artifactId), pomPath) - } - - //#nosec G703 G306 -- path from scan workflow; 0644 for VCS-tracked sources. - if err = os.WriteFile(pomPath, currentContent, 0644); err != nil { - return fmt.Errorf("failed to write %s: %w", pomPath, err) - } - return nil -} - -func parseDependencyName(dependencyName string) (groupId, artifactId string, err error) { - parts := strings.Split(dependencyName, mavenDependencySeparator) - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid Maven dependency name: %s. Expected format 'groupId:artifactId'", dependencyName) - } - return parts[0], parts[1], nil -} - -func toDependencyName(groupId, artifactId string) string { - return groupId + mavenDependencySeparator + artifactId -} - -func (m *MavenPackageUpdater) updateInParent(project *mavenProject, groupId, artifactId, fixedVersion string, content []byte) (bool, []byte) { - if project.Parent == nil { - return false, content - } - - if project.Parent.GroupId == groupId && project.Parent.ArtifactId == artifactId { - pattern := regexp.MustCompile(`(?s)(\s*` + regexp.QuoteMeta(groupId) + `\s*` + regexp.QuoteMeta(artifactId) + `\s*)[^<]+()`) - newContent := pattern.ReplaceAll(content, []byte("${1}"+fixedVersion+"${2}")) - if !bytes.Equal(content, newContent) { - log.Debug("Updated parent", toDependencyName(groupId, artifactId), "to", fixedVersion) - return true, newContent - } - } - return false, content -} - -func (m *MavenPackageUpdater) updateInDependencies(project *mavenProject, deps []mavenDep, groupId, artifactId, fixedVersion string, content []byte) (bool, []byte) { - for _, dep := range deps { - if dep.GroupId == groupId && dep.ArtifactId == artifactId { - if propertyName, isProperty := extractPropertyName(dep.Version); isProperty { - return m.updateProperty(project, propertyName, fixedVersion, content) - } - - pattern := regexp.MustCompile(`(?s)(` + regexp.QuoteMeta(groupId) + `\s*` + regexp.QuoteMeta(artifactId) + `\s*)[^<]+()`) - newContent := pattern.ReplaceAll(content, []byte("${1}"+fixedVersion+"${2}")) - if !bytes.Equal(content, newContent) { - log.Debug("Updated dependency", toDependencyName(groupId, artifactId), "to", fixedVersion) - return true, newContent - } - } - } - return false, content -} - -func extractPropertyName(version string) (string, bool) { - if strings.HasPrefix(version, propertyPrefix) && strings.HasSuffix(version, propertySuffix) { - return strings.TrimSuffix(strings.TrimPrefix(version, propertyPrefix), propertySuffix), true - } - return "", false -} - -func (m *MavenPackageUpdater) updateProperty(project *mavenProject, propertyName, newValue string, content []byte) (bool, []byte) { - if project.Properties == nil { - return false, content - } - - for _, prop := range project.Properties.Props { - if prop.XMLName.Local == propertyName { - pattern := regexp.MustCompile(`(<` + regexp.QuoteMeta(propertyName) + `>)[^<]+()`) - newContent := pattern.ReplaceAll(content, []byte("${1}"+newValue+"${2}")) - if !bytes.Equal(content, newContent) { - return true, newContent - } - } - } - return false, content -} diff --git a/packageupdaters/mavenpackageupdater_test.go b/packageupdaters/mavenpackageupdater_test.go deleted file mode 100644 index d0c0397d2..000000000 --- a/packageupdaters/mavenpackageupdater_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package packageupdaters - -import ( - "errors" - "os" - "path/filepath" - "testing" - - biutils "github.com/jfrog/build-info-go/utils" - "github.com/jfrog/jfrog-cli-security/utils/formats" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/stretchr/testify/assert" - - "github.com/jfrog/frogbot/v3/utils" -) - -func TestMavenUpdateDependency(t *testing.T) { - testProjectPath := filepath.Join("..", "testdata", "packageupdaters") - currDir, err := os.Getwd() - assert.NoError(t, err) - - propertyPOM := ` - - 4.0.0 - test - test - 1.0 - - - 2.9.8 - - - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - -` - - parentPOM := ` - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.5.0 - - - test - test - 1.0 -` - - testCases := []struct { - name string - customPOM string // if non-empty, overwrites pom.xml after copying testdata - vulnDetails *utils.VulnerabilityDetails - expectedContains []string - expectedNotContain []string - }{ - { - name: "RegularDependency", - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "1.1.5", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: techutils.Maven, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "org.jfrog.filespecs:file-specs-java", - Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: "pom.xml"}}}}, - }, - }, - }, - expectedContains: []string{"1.1.5"}, - expectedNotContain: []string{"1.1.1"}, - }, - { - name: "DependencyManagement", - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "2.15.0", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: techutils.Maven, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "com.fasterxml.jackson.core:jackson-core", - Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: "pom.xml"}}}}, - }, - }, - }, - expectedContains: []string{"2.15.0"}, - expectedNotContain: []string{"2.13.4"}, - }, - { - name: "PropertyVersion", - customPOM: propertyPOM, - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "2.13.0", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: techutils.Maven, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "com.fasterxml.jackson.core:jackson-databind", - Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: "pom.xml"}}}}, - }, - }, - }, - expectedContains: []string{"2.13.0"}, - expectedNotContain: []string{"2.9.8"}, - }, - { - name: "ParentPOM", - customPOM: parentPOM, - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "2.7.0", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: techutils.Maven, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "org.springframework.boot:spring-boot-starter-parent", - Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: "pom.xml"}}}}, - }, - }, - }, - expectedContains: []string{"2.7.0"}, - expectedNotContain: []string{"2.5.0"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "maven-test-*") - assert.NoError(t, err) - defer func() { - assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) - }() - - assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil)) - if tc.customPOM != "" { - assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "pom.xml"), []byte(tc.customPOM), 0644)) - } - assert.NoError(t, os.Chdir(tmpDir)) - defer func() { - assert.NoError(t, os.Chdir(currDir)) - }() - - updater := &MavenPackageUpdater{} - err = updater.UpdateDependency(tc.vulnDetails) - assert.NoError(t, err) - - modifiedPom, err := os.ReadFile("pom.xml") - assert.NoError(t, err) - content := string(modifiedPom) - for _, s := range tc.expectedContains { - assert.Contains(t, content, s) - } - for _, s := range tc.expectedNotContain { - assert.NotContains(t, content, s) - } - }) - } -} - -func TestMavenUpdateDependencyErrors(t *testing.T) { - testProjectPath := filepath.Join("..", "testdata", "packageupdaters") - currDir, err := os.Getwd() - assert.NoError(t, err) - - testCases := []struct { - name string - vulnDetails *utils.VulnerabilityDetails - useTestData bool - assertErr func(t *testing.T, err error) - }{ - { - name: "DependencyNotFound", - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "1.0.0", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: techutils.Maven, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "com.nonexistent:package", - Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: "pom.xml"}}}}, - }, - }, - }, - useTestData: true, - assertErr: func(t *testing.T, err error) { - assert.Error(t, err) - assert.Contains(t, err.Error(), "com.nonexistent:package") - }, - }, - { - name: "IndirectDependencyNotSupported", - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "1.0.0", - IsDirectDependency: false, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ - Technology: techutils.Maven, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "org.springframework:spring-core", - Components: []formats.ComponentRow{{Evidences: []formats.Location{{File: "pom.xml"}}}}, - }, - }, - }, - useTestData: false, - assertErr: func(t *testing.T, err error) { - assert.Error(t, err) - var unsupportedErr *utils.ErrUnsupportedFix - assert.True(t, errors.As(err, &unsupportedErr)) - assert.Equal(t, utils.IndirectDependencyFixNotSupported, unsupportedErr.ErrorType) - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.useTestData { - tmpDir, err := os.MkdirTemp("", "maven-test-*") - assert.NoError(t, err) - defer func() { - assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) - }() - assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil)) - assert.NoError(t, os.Chdir(tmpDir)) - defer func() { - assert.NoError(t, os.Chdir(currDir)) - }() - } - updater := &MavenPackageUpdater{} - err := updater.UpdateDependency(tc.vulnDetails) - tc.assertErr(t, err) - }) - } -} diff --git a/packageupdaters/npmpackageupdater.go b/packageupdaters/npmpackageupdater.go deleted file mode 100644 index 67427c026..000000000 --- a/packageupdaters/npmpackageupdater.go +++ /dev/null @@ -1,182 +0,0 @@ -package packageupdaters - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/jfrog/jfrog-client-go/utils/log" - - "github.com/jfrog/frogbot/v3/utils" -) - -const ( - ciEnv = "CI" - configIgnoreScriptsEnv = "NPM_CONFIG_IGNORE_SCRIPTS" - configAuditEnv = "NPM_CONFIG_AUDIT" - configFundEnv = "NPM_CONFIG_FUND" - configLevelEnv = "NPM_CONFIG_LOGLEVEL" - - npmPackageLockOnlyFlag = "--package-lock-only" - npmIgnoreScriptsFlag = "--ignore-scripts" - npmNoAuditFlag = "--no-audit" - npmLegacyPeerDepsFlag = "--legacy-peer-deps" - npmNoFundFlag = "--no-fund" - - npmLockFileName = "package-lock.json" - - npmEreresolveErrorPrefix = "ERESOLVE" -) - -var npmInstallEnvVars = map[string]string{ - configIgnoreScriptsEnv: "true", - configAuditEnv: "false", - configFundEnv: "false", - configLevelEnv: "error", - ciEnv: "true", -} - -type NpmPackageUpdater struct { - CommonPackageUpdater -} - -func (npm *NpmPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return npm.updateDirectDependency(vulnDetails) - } - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } -} - -func (npm *NpmPackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) error { - descriptorPaths := npm.CollectVulnerabilityDescriptorPaths(vulnDetails, []string{nodePackageJSONFileName}, []string{nodeModulesDirName}) - if len(descriptorPaths) == 0 { - return fmt.Errorf("no descriptor evidence was found for package %s", vulnDetails.ImpactedDependencyName) - } - - originalWd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - - var failingDescriptors []string - for _, descriptorPath := range descriptorPaths { - if fixErr := npm.fixVulnerabilityAndRegenerateLock(vulnDetails, descriptorPath, originalWd); fixErr != nil { - failedFixErrorMsg := fmt.Errorf("failed to fix '%s' in descriptor '%s': %w", vulnDetails.ImpactedDependencyName, descriptorPath, fixErr) - log.Warn(failedFixErrorMsg.Error()) - err = errors.Join(err, failedFixErrorMsg) - failingDescriptors = append(failingDescriptors, descriptorPath) - } - } - if err != nil { - return fmt.Errorf("encountered errors while fixing '%s' vulnerability in descriptors [%s]: %w", vulnDetails.ImpactedDependencyName, strings.Join(failingDescriptors, ", "), err) - } - - return nil -} - -func (npm *NpmPackageUpdater) fixVulnerabilityAndRegenerateLock(vulnDetails *utils.VulnerabilityDetails, descriptorPath string, originalWd string) error { - backupContent, err := npm.UpdatePackageJSONDescriptor(descriptorPath, vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion) - if err != nil { - return err - } - - descriptorDir := filepath.Dir(descriptorPath) - lockFilePath := filepath.Join(descriptorDir, npmLockFileName) - - lockFileTracked, checkErr := utils.IsFileTrackedByGit(lockFilePath, originalWd) - if checkErr != nil { - log.Debug(fmt.Sprintf("Failed to check if lock file is tracked in git: %s. Proceeding with lock file regeneration.", checkErr.Error())) - lockFileTracked = true - } - - if !lockFileTracked { - log.Debug(fmt.Sprintf("Lock file '%s' is not tracked in git, skipping lock file regeneration", lockFilePath)) - return nil - } - - if err = npm.regenerateLockfile(vulnDetails, descriptorPath, originalWd, backupContent); err != nil { - return err - } - - log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s' in descriptor '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion, descriptorPath)) - return nil -} - -func (npm *NpmPackageUpdater) regenerateLockfile(vulnDetails *utils.VulnerabilityDetails, descriptorPath, originalWd string, backupContent []byte) error { - return npm.withDescriptorWorkingDir(descriptorPath, originalWd, func() error { - if err := npm.regenerateLockFileWithRetry(); err != nil { - log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) - //#nosec G306 -- 0644 is correct for a checked-out source file. - if rollbackErr := os.WriteFile(descriptorPath, backupContent, 0644); rollbackErr != nil { - return fmt.Errorf("failed to rollback descriptor after lock file regeneration failure: %w (original error: %v)", rollbackErr, err) - } - return err - } - return nil - }) -} - -func (npm *NpmPackageUpdater) getFixedDescriptor(content []byte, packageName, newVersion, descriptorPath string) ([]byte, error) { - return npm.GetFixedPackageJSONManifest(content, packageName, newVersion, descriptorPath) -} - -func (npm *NpmPackageUpdater) regenerateLockFileWithRetry() error { - err := npm.runNpmInstall(false) - if err != nil { - if strings.Contains(err.Error(), npmEreresolveErrorPrefix) { - log.Debug(fmt.Sprintf("First npm install attempt failed due to peer dependency conflict. Retrying with %s...", npmLegacyPeerDepsFlag)) - if err = npm.runNpmInstall(true); err != nil { - return fmt.Errorf("npm install failed after retry with %s: %w", npmLegacyPeerDepsFlag, err) - } - return nil - } - log.Debug(fmt.Sprintf("First npm install attempt failed: %s. Retrying...", err.Error())) - if err = npm.runNpmInstall(false); err != nil { - return fmt.Errorf("npm install failed after retry: %w", err) - } - } - return nil -} - -func (npm *NpmPackageUpdater) runNpmInstall(useLegacyPeerDeps bool) error { - args := []string{ - "install", - npmPackageLockOnlyFlag, - npmIgnoreScriptsFlag, - npmNoAuditFlag, - npmNoFundFlag, - } - if useLegacyPeerDeps { - args = append(args, npmLegacyPeerDepsFlag) - } - - fullCommand := "npm " + strings.Join(args, " ") - log.Debug(fmt.Sprintf("Running '%s'", fullCommand)) - - ctx, cancel := context.WithTimeout(context.Background(), nodePackageManagerInstallTimeout) - defer cancel() - - //#nosec G204 -- False positive - the subprocess only runs after the user's approval - cmd := exec.CommandContext(ctx, "npm", args...) - - cmd.Env = npm.buildEnvWithOverrides(npmInstallEnvVars) - output, err := cmd.CombinedOutput() - - if errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) { - return fmt.Errorf("npm install timed out after %v", nodePackageManagerInstallTimeout) - } - - if err != nil { - return fmt.Errorf("npm install failed: %w\nOutput: %s", err, string(output)) - } - - return nil -} diff --git a/packageupdaters/npmpackageupdater_test.go b/packageupdaters/npmpackageupdater_test.go deleted file mode 100644 index 2d3d3ffd0..000000000 --- a/packageupdaters/npmpackageupdater_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package packageupdaters - -import ( - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetFixedDescriptor(t *testing.T) { - npm := &NpmPackageUpdater{} - - testcases := []struct { - name string - originalContent string - packageName string - newVersion string - expectedContent string - expectError bool - }{ - { - name: "update exact version", - originalContent: `{"dependencies": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"dependencies": {"minimist": "1.2.6"}}`, - expectError: false, - }, - { - name: "update version with caret prefix - removes prefix", - originalContent: `{"dependencies": {"lodash": "^4.17.0"}}`, - packageName: "lodash", - newVersion: "4.17.21", - expectedContent: `{"dependencies": {"lodash": "4.17.21"}}`, - expectError: false, - }, - { - name: "update version with tilde prefix - removes prefix", - originalContent: `{"dependencies": {"express": "~4.18.0"}}`, - packageName: "express", - newVersion: "4.18.2", - expectedContent: `{"dependencies": {"express": "4.18.2"}}`, - expectError: false, - }, - { - name: "update scoped package", - originalContent: `{"dependencies": {"@types/node": "18.0.0"}}`, - packageName: "@types/node", - newVersion: "18.11.0", - expectedContent: `{"dependencies": {"@types/node": "18.11.0"}}`, - expectError: false, - }, - { - name: "package not found", - originalContent: `{"dependencies": {"lodash": "4.17.0"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: "", - expectError: true, - }, - { - name: "update in devDependencies", - originalContent: `{"devDependencies": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"devDependencies": {"minimist": "1.2.6"}}`, - expectError: false, - }, - { - name: "update in optionalDependencies", - originalContent: `{"optionalDependencies": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"optionalDependencies": {"minimist": "1.2.6"}}`, - expectError: false, - }, - { - name: "update in overrides section", - originalContent: `{"dependencies": {"express": "4.18.0"}, "overrides": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"dependencies": {"express": "4.18.0"}, "overrides": {"minimist": "1.2.6"}}`, - expectError: false, - }, - { - name: "update in both dependencies and overrides", - originalContent: `{"dependencies": {"minimist": "1.2.5"}, "overrides": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"dependencies": {"minimist": "1.2.6"}, "overrides": {"minimist": "1.2.6"}}`, - expectError: false, - }, - { - name: "skip peerDependencies section - package only in peerDependencies", - originalContent: `{"dependencies": {"express": "4.18.0"}, "peerDependencies": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: "", - expectError: true, - }, - { - name: "skip peerDependencies section - package in both dependencies and peerDependencies", - originalContent: `{"dependencies": {"minimist": "1.2.5"}, "peerDependencies": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"dependencies": {"minimist": "1.2.6"}, "peerDependencies": {"minimist": "1.2.5"}}`, - expectError: false, - }, - { - name: "update in multiple allowed sections", - originalContent: `{"dependencies": {"minimist": "1.2.5"}, "devDependencies": {"minimist": "1.2.5"}}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: `{"dependencies": {"minimist": "1.2.6"}, "devDependencies": {"minimist": "1.2.6"}}`, - expectError: false, - }, - { - name: "package name with dot", - originalContent: `{"dependencies": {"vue.config": "1.0.0"}}`, - packageName: "vue.config", - newVersion: "2.0.0", - expectedContent: `{"dependencies": {"vue.config": "2.0.0"}}`, - expectError: false, - }, - { - name: "no dependency sections", - originalContent: `{"name": "test", "version": "1.0.0"}`, - packageName: "minimist", - newVersion: "1.2.6", - expectedContent: "", - expectError: true, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - result, err := npm.getFixedDescriptor([]byte(tc.originalContent), tc.packageName, tc.newVersion, "package.json") - - if tc.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tc.expectedContent, string(result)) - } - }) - } -} - -func TestEscapeJsonPathKey(t *testing.T) { - testcases := []struct { - name string - input string - expected string - }{ - { - name: "simple package name", - input: "lodash", - expected: "lodash", - }, - { - name: "scoped package", - input: "@types/node", - expected: "@types/node", - }, - { - name: "package with dot", - input: "vue.config", - expected: "vue\\.config", - }, - { - name: "package with multiple dots", - input: "some.package.name", - expected: "some\\.package\\.name", - }, - { - name: "package with asterisk", - input: "package*name", - expected: "package\\*name", - }, - { - name: "package with question mark", - input: "package?name", - expected: "package\\?name", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - result := escapeJsonPathKey(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestBuildIsolatedEnv(t *testing.T) { - testcases := []struct { - name string - predefineEnv bool - }{ - { - name: "sets required env vars", - predefineEnv: false, - }, - { - name: "overrides conflicting user env vars", - predefineEnv: true, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - if tc.predefineEnv { - originalCI := os.Getenv(ciEnv) - defer func() { - assert.NoError(t, os.Setenv(ciEnv, originalCI)) - }() - assert.NoError(t, os.Setenv(ciEnv, "false")) - } - - npm := &NpmPackageUpdater{} - env := npm.buildEnvWithOverrides(npmInstallEnvVars) - - envMap := make(map[string]string) - envCount := make(map[string]int) - for _, e := range env { - parts := strings.SplitN(e, "=", 2) - if len(parts) == 2 { - envMap[parts[0]] = parts[1] - envCount[parts[0]]++ - } - } - - assert.Equal(t, "true", envMap[configIgnoreScriptsEnv]) - assert.Equal(t, "false", envMap[configAuditEnv]) - assert.Equal(t, "false", envMap[configFundEnv]) - assert.Equal(t, "error", envMap[configLevelEnv]) - assert.Equal(t, "true", envMap[ciEnv]) - - if tc.predefineEnv { - assert.Equal(t, 1, envCount[ciEnv], "CI should appear exactly once") - } - }) - } -} diff --git a/packageupdaters/nugetpackageupdater.go b/packageupdaters/nugetpackageupdater.go deleted file mode 100644 index fa62da1bf..000000000 --- a/packageupdaters/nugetpackageupdater.go +++ /dev/null @@ -1,98 +0,0 @@ -package packageupdaters - -import ( - "errors" - "fmt" - "github.com/jfrog/frogbot/v3/utils" - "os" - "path" - "regexp" - "strings" -) - -const ( - dotnetUpdateCmdPackageExtraArg = "package" - dotnetNoRestoreFlag = "--no-restore" - dotnetAssetsFilesSuffix = "csproj" - dotnetDependencyRegexpPattern = "include=[\\\"|\\']%s[\\\"|\\']\\s*version=[\\\"|\\']%s[\\\"|\\']" -) - -type NugetPackageUpdater struct { - CommonPackageUpdater -} - -func (nph *NugetPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return nph.updateDirectDependency(vulnDetails) - } - - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } -} - -func (nph *NugetPackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - var descriptorFilesFullPaths []string - descriptorFilesFullPaths, err = nph.GetAllDescriptorFilesFullPaths([]string{dotnetAssetsFilesSuffix}) - if err != nil { - return - } - - wd, err := os.Getwd() - if err != nil { - err = fmt.Errorf("failed to get current working directory: %s", err.Error()) - return - } - - vulnRegexpCompiler := BuildPackageWithVersionRegex(vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, dotnetDependencyRegexpPattern) - var isAnyFileChanged bool - - for _, descriptorFilePath := range descriptorFilesFullPaths { - var isFileChanged bool - isFileChanged, err = nph.fixVulnerabilityIfExists(vulnDetails, descriptorFilePath, wd, vulnRegexpCompiler) - if err != nil { - err = fmt.Errorf("failed to update asset file '%s': %s", descriptorFilePath, err.Error()) - return - } - - // We use logic OR in order to keep track whether any asset file changed during the fix process - isAnyFileChanged = isAnyFileChanged || isFileChanged - } - - if !isAnyFileChanged { - err = fmt.Errorf("impacted package '%s' was not found or could not be fixed in all descriptor files", vulnDetails.ImpactedDependencyName) - } - return -} - -func (nph *NugetPackageUpdater) fixVulnerabilityIfExists(vulnDetails *utils.VulnerabilityDetails, descriptorFilePath, originalWd string, vulnRegexpCompiler *regexp.Regexp) (isFileChanged bool, err error) { - modulePath := path.Dir(descriptorFilePath) - - var fileData []byte - //#nosec G304 -- descriptor path from scan workflow. - fileData, err = os.ReadFile(descriptorFilePath) - if err != nil { - err = fmt.Errorf("failed to read file '%s': %s", descriptorFilePath, err.Error()) - return - } - - if matchingRow := vulnRegexpCompiler.FindString(strings.ToLower(string(fileData))); matchingRow != "" { - err = os.Chdir(modulePath) - if err != nil { - err = fmt.Errorf("failed to change directory to '%s': %s", modulePath, err.Error()) - return - } - defer func() { - err = errors.Join(err, os.Chdir(originalWd)) - }() - - err = nph.CommonPackageUpdater.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand(), dotnetUpdateCmdPackageExtraArg, dotnetNoRestoreFlag) - if err != nil { - return - } - isFileChanged = true - } - return -} diff --git a/packageupdaters/pnpmpackageupdater.go b/packageupdaters/pnpmpackageupdater.go deleted file mode 100644 index 9aeacd9cb..000000000 --- a/packageupdaters/pnpmpackageupdater.go +++ /dev/null @@ -1,161 +0,0 @@ -package packageupdaters - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/jfrog/jfrog-client-go/utils/log" - - "github.com/jfrog/frogbot/v3/utils" -) - -const ( - pnpmLockFileName = "pnpm-lock.yaml" - pnpmLockfileOnlyFlag = "--lockfile-only" - pnpmIgnoreScriptsFlag = "--ignore-scripts" - pnpmNoFrozenLockfileFlag = "--no-frozen-lockfile" - pnpmFrozenLockfileEnv = "PNPM_FROZEN_LOCKFILE" -) - -// pnpmLockfileInstallEnvOverrides are merged into the process environment for `pnpm install` when refreshing pnpm-lock.yaml. -// PNPM_FROZEN_LOCKFILE=false — CI often implies a frozen lockfile; after editing package.json the lock must be allowed to change. -// NPM_CONFIG_LOGLEVEL=error — same spirit as npm lock regen: avoid noisy info-level logs. -// CI=true — stable, non-interactive behavior consistent with other Node package manager subprocesses here. -// COREPACK_INTEGRITY_KEYS is not set here; envWithCorepackIntegrityWorkaround appends it (see TestEnvWithCorepackIntegrityWorkaround, TestPnpmLockRegenerationEnv). -func pnpmLockfileInstallEnvOverrides() map[string]string { - return map[string]string{ - pnpmFrozenLockfileEnv: "false", - configLevelEnv: "error", - ciEnv: "true", - } -} - -func pnpmFilterCoordinateStyleDescriptorPaths(paths []string) []string { - if len(paths) == 0 { - return paths - } - out := make([]string, 0, len(paths)) - for _, p := range paths { - if !evidencePathLooksLikeNpmPackageCoordinate(p) { - out = append(out, p) - } - } - return out -} - -type PnpmPackageUpdater struct { - CommonPackageUpdater -} - -func (pnpm *PnpmPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return pnpm.updateDirectDependency(vulnDetails) - } - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } -} - -func (pnpm *PnpmPackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) error { - descriptorPaths := pnpm.CollectVulnerabilityDescriptorPaths(vulnDetails, []string{nodePackageJSONFileName}, []string{nodeModulesDirName}) - descriptorPaths = pnpmFilterCoordinateStyleDescriptorPaths(descriptorPaths) - if len(descriptorPaths) == 0 { - return fmt.Errorf("no descriptor evidence was found for package %s", vulnDetails.ImpactedDependencyName) - } - - originalWd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - - var failingDescriptors []string - for _, descriptorPath := range descriptorPaths { - if fixErr := pnpm.fixVulnerabilityAndRegenerateLock(vulnDetails, descriptorPath, originalWd); fixErr != nil { - failedFixErrorMsg := fmt.Errorf("failed to fix '%s' in descriptor '%s': %w", vulnDetails.ImpactedDependencyName, descriptorPath, fixErr) - log.Warn(failedFixErrorMsg.Error()) - err = errors.Join(err, failedFixErrorMsg) - failingDescriptors = append(failingDescriptors, descriptorPath) - } - } - if err != nil { - return fmt.Errorf("encountered errors while fixing '%s' vulnerability in descriptors [%s]: %w", vulnDetails.ImpactedDependencyName, strings.Join(failingDescriptors, ", "), err) - } - - return nil -} - -func (pnpm *PnpmPackageUpdater) fixVulnerabilityAndRegenerateLock(vulnDetails *utils.VulnerabilityDetails, descriptorPath string, originalWd string) error { - backupContent, err := pnpm.UpdatePackageJSONDescriptor(descriptorPath, vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion) - if err != nil { - return err - } - - descriptorDir := filepath.Dir(descriptorPath) - lockFilePath := filepath.Join(descriptorDir, pnpmLockFileName) - - lockFileTracked, checkErr := utils.IsFileTrackedByGit(lockFilePath, originalWd) - if checkErr != nil { - log.Debug(fmt.Sprintf("Failed to check if lock file is tracked in git: %s. Proceeding with lock file regeneration.", checkErr.Error())) - lockFileTracked = true - } - - if !lockFileTracked { - log.Debug(fmt.Sprintf("Lock file '%s' is not tracked in git, skipping lock file regeneration", lockFilePath)) - return nil - } - - if err = pnpm.regenerateLockfile(vulnDetails, descriptorPath, originalWd, backupContent); err != nil { - return err - } - - log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s' in descriptor '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion, descriptorPath)) - return nil -} - -func (pnpm *PnpmPackageUpdater) regenerateLockfile(vulnDetails *utils.VulnerabilityDetails, descriptorPath, originalWd string, backupContent []byte) error { - return pnpm.withDescriptorWorkingDir(descriptorPath, originalWd, func() error { - if err := pnpm.runPnpmInstallLockOnly(); err != nil { - log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) - //#nosec G306 -- 0644 is correct for a checked-out source file. - if rollbackErr := os.WriteFile(descriptorPath, backupContent, 0644); rollbackErr != nil { - return fmt.Errorf("failed to rollback descriptor after lock file regeneration failure: %w (original error: %v)", rollbackErr, err) - } - return err - } - return nil - }) -} - -func (pnpm *PnpmPackageUpdater) runPnpmInstallLockOnly() error { - args := []string{ - "install", - pnpmLockfileOnlyFlag, - pnpmIgnoreScriptsFlag, - pnpmNoFrozenLockfileFlag, - } - fullCommand := "pnpm " + strings.Join(args, " ") - log.Debug(fmt.Sprintf("Running '%s'", fullCommand)) - - ctx, cancel := context.WithTimeout(context.Background(), nodePackageManagerInstallTimeout) - defer cancel() - - //#nosec G204 -- False positive - the subprocess only runs after the user's approval - cmd := exec.CommandContext(ctx, "pnpm", args...) - cmd.Env = envWithCorepackIntegrityWorkaround(pnpm.buildEnvWithOverrides(pnpmLockfileInstallEnvOverrides())) - - output, err := cmd.CombinedOutput() - if errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded) { - return fmt.Errorf("pnpm install timed out after %v", nodePackageManagerInstallTimeout) - } - if err != nil { - return fmt.Errorf("pnpm install failed: %w\nOutput: %s", err, string(output)) - } - return nil -} diff --git a/packageupdaters/pnpmpackageupdater_test.go b/packageupdaters/pnpmpackageupdater_test.go deleted file mode 100644 index 8f5d3cb98..000000000 --- a/packageupdaters/pnpmpackageupdater_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package packageupdaters - -import ( - "strings" - "testing" - - "github.com/jfrog/jfrog-cli-security/utils/techutils" - "github.com/stretchr/testify/assert" -) - -func TestEvidencePathLooksLikeNpmPackageCoordinate(t *testing.T) { - t.Parallel() - tests := []struct { - path string - wantTrue bool - }{ - {"lodash@4.17.19/package.json", true}, - {"axios@0.21.1/package.json", true}, - {"nested/pkg@1.0.0-rc.1/sub/package.json", true}, - {"package.json", false}, - {"apps/web/package.json", false}, - {"node_modules/@types/node/package.json", false}, - {"node_modules/@scope/pkg/package.json", false}, - {"@types/node/package.json", false}, - } - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - t.Parallel() - assert.Equal(t, tt.wantTrue, evidencePathLooksLikeNpmPackageCoordinate(tt.path), tt.path) - }) - } -} - -func TestPnpmFilterCoordinateStyleDescriptorPaths(t *testing.T) { - t.Parallel() - in := []string{ - "lodash@4.17.19/package.json", - "axios@0.21.1/package.json", - "package.json", - "apps/web/package.json", - "node_modules/@types/node/package.json", - } - want := []string{"package.json", "apps/web/package.json", "node_modules/@types/node/package.json"} - assert.ElementsMatch(t, want, pnpmFilterCoordinateStyleDescriptorPaths(in)) -} - -func TestPnpmCollectLeavesNpmParityThenPnpmFilterDropsCoordinates(t *testing.T) { - t.Parallel() - pnpm := &PnpmPackageUpdater{} - vuln := createVulnerabilityDetails(techutils.Pnpm, "lodash", "4.17.19", "4.17.21", true, - "lodash@4.17.19/package.json", "axios@0.21.1/package.json", "package.json") - raw := pnpm.CollectVulnerabilityDescriptorPaths(vuln, []string{nodePackageJSONFileName}, []string{nodeModulesDirName}) - assert.ElementsMatch(t, []string{"lodash@4.17.19/package.json", "axios@0.21.1/package.json", "package.json"}, raw) - assert.ElementsMatch(t, []string{"package.json"}, pnpmFilterCoordinateStyleDescriptorPaths(raw)) -} - -func TestPnpmLockRegenerationEnv(t *testing.T) { - t.Parallel() - pnpm := &PnpmPackageUpdater{} - env := envWithCorepackIntegrityWorkaround(pnpm.buildEnvWithOverrides(pnpmLockfileInstallEnvOverrides())) - envMap := make(map[string]string) - for _, e := range env { - parts := strings.SplitN(e, "=", 2) - if len(parts) == 2 { - envMap[parts[0]] = parts[1] - } - } - assert.Equal(t, "false", envMap[pnpmFrozenLockfileEnv]) - assert.Equal(t, "error", envMap[configLevelEnv]) - assert.Equal(t, "true", envMap[ciEnv]) - assert.Equal(t, "0", envMap["COREPACK_INTEGRITY_KEYS"]) -} diff --git a/packageupdaters/pythonpackageupdater.go b/packageupdaters/pythonpackageupdater.go deleted file mode 100644 index f634a678d..000000000 --- a/packageupdaters/pythonpackageupdater.go +++ /dev/null @@ -1,119 +0,0 @@ -package packageupdaters - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/jfrog/jfrog-cli-security/utils/techutils" - - "github.com/jfrog/frogbot/v3/utils" -) - -const ( - defaultRequirementFile = "requirements.txt" - // Package names are case-insensitive with this prefix - PythonPackageRegexPrefix = "(?i)" - // Match all possible operators and versions syntax - PythonPackageRegexSuffix = "\\s*(([\\=\\<\\>\\~]=)|([\\>\\<]))\\s*(\\.|\\d)*(\\d|(\\.\\*))(\\,\\s*(([\\=\\<\\>\\~]=)|([\\>\\<])).*\\s*(\\.|\\d)*(\\d|(\\.\\*)))?" -) - -type PythonPackageUpdater struct { - pipRequirementsFile string - CommonPackageUpdater -} - -func (py *PythonPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return py.updateDirectDependency(vulnDetails) - } - - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } -} - -func (py *PythonPackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - switch vulnDetails.Technology { - case techutils.Poetry: - return py.handlePoetry(vulnDetails) - case techutils.Pip: - return py.handlePip(vulnDetails) - case techutils.Pipenv: - return py.CommonPackageUpdater.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand()) - default: - return errors.New("unknown python package manger: " + vulnDetails.Technology.GetPackageType()) - } -} - -func (py *PythonPackageUpdater) handlePoetry(vulnDetails *utils.VulnerabilityDetails) (err error) { - if err = py.CommonPackageUpdater.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand()); err != nil { - return - } - return runPackageMangerCommand(techutils.Poetry.GetExecCommandName(), techutils.Poetry.String(), []string{"update"}) -} - -func (py *PythonPackageUpdater) handlePip(vulnDetails *utils.VulnerabilityDetails) (err error) { - var fixedFile string - fixedPackage := vulnDetails.ImpactedDependencyName + "==" + vulnDetails.SuggestedFixedVersion - currentFile, err := py.tryGetRequirementFile() - if err != nil { - return errors.New("failed to read pip requirements file: " + err.Error()) - } - re := regexp.MustCompile(PythonPackageRegexPrefix + "(" + vulnDetails.ImpactedDependencyName + "|" + strings.ToLower(vulnDetails.ImpactedDependencyName) + ")" + PythonPackageRegexSuffix) - if packageToReplace := re.FindString(currentFile); packageToReplace != "" { - fixedFile = strings.Replace(currentFile, packageToReplace, strings.ToLower(fixedPackage), 1) - } - if fixedFile == "" { - return fmt.Errorf("impacted package %s not found, fix failed", vulnDetails.ImpactedDependencyName) - } - //#nosec G703 -- False positive - the path is determined by internal file scanning, not user input, and was already validated by the preceding Stat call. - if err = os.WriteFile(py.pipRequirementsFile, []byte(fixedFile), 0600); err != nil { - err = fmt.Errorf("an error occured while writing the fixed version of %s to the requirements file:\n%s", vulnDetails.SuggestedFixedVersion, err.Error()) - } - return -} - -func (py *PythonPackageUpdater) tryGetRequirementFile() (string, error) { - if py.pipRequirementsFile != "" { - fileContent, err := py.tryReadRequirementFile(py.pipRequirementsFile) - if err != nil { - return "", err - } - return fileContent, nil - } else { - // if we don't have a value in py.pipRequirementsFile - we try first setup.py and then requirements.txt - py.pipRequirementsFile = "setup.py" - fileContent, err := py.tryReadRequirementFile(py.pipRequirementsFile) - if err != nil { - py.pipRequirementsFile = "requirements.txt" - fileContent, err = py.tryReadRequirementFile(py.pipRequirementsFile) - if err != nil { - return "", err - } - return fileContent, nil - } - return fileContent, nil - } -} - -func (py *PythonPackageUpdater) tryReadRequirementFile(file string) (string, error) { - wd, err := os.Getwd() - if err != nil { - return "", err - } - fullPath := filepath.Join(wd, file) - if !strings.HasPrefix(filepath.Clean(fullPath), wd) { - return "", errors.New("wrong requirements file input: " + fullPath) - } - data, err := os.ReadFile(filepath.Clean(file)) - if err != nil { - return "", errors.New("an error occurred while attempting to read the requirements file:\n" + err.Error()) - } - return string(data), nil -} diff --git a/packageupdaters/pythonpackageupdater_test.go b/packageupdaters/pythonpackageupdater_test.go deleted file mode 100644 index 2fcfaa3b8..000000000 --- a/packageupdaters/pythonpackageupdater_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package packageupdaters - -import ( - "regexp" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPipPackageRegex(t *testing.T) { - var pipPackagesRegexTests = []pipPackageRegexTest{ - {"oslo.config", "oslo.config>=1.12.1,<1.13"}, - {"oslo.utils", "oslo.utils<5.0,>=4.0.0"}, - {"paramiko", "paramiko==2.7.2"}, - {"passlib", "passlib<=1.7.4"}, - {"PassLib", "passlib<=1.7.4"}, - {"prance", "prance>=0.9.0"}, - {"prompt-toolkit", "prompt-toolkit~=1.0.15"}, - {"pyinotify", "pyinotify>0.9.6"}, - {"pyjwt", "pyjwt>1.7.1"}, - {"PyJWT", "pyjwt>1.7.1"}, - {"urllib3", "urllib3 > 1.1.9, < 1.5.*"}, - } - for _, pack := range pipPackagesRegexTests { - re := regexp.MustCompile(PythonPackageRegexPrefix + "(" + pack.packageName + "|" + strings.ToLower(pack.packageName) + ")" + PythonPackageRegexSuffix) - found := re.FindString(requirementsFile) - assert.Equal(t, pack.expectedRequirement, strings.ToLower(found)) - } -} diff --git a/packageupdaters/unsupportedpackageupdater.go b/packageupdaters/unsupportedpackageupdater.go deleted file mode 100644 index d1fe66cb1..000000000 --- a/packageupdaters/unsupportedpackageupdater.go +++ /dev/null @@ -1,13 +0,0 @@ -package packageupdaters - -import ( - "errors" - "github.com/jfrog/frogbot/v3/utils" -) - -type UnsupportedPackageUpdater struct { -} - -func (uph *UnsupportedPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - return errors.New("frogbot currently does not support opening a pull request that fixes vulnerabilities in " + vulnDetails.Technology.ToFormal()) -} diff --git a/packageupdaters/yarnpackageupdater.go b/packageupdaters/yarnpackageupdater.go deleted file mode 100644 index f3e831a72..000000000 --- a/packageupdaters/yarnpackageupdater.go +++ /dev/null @@ -1,82 +0,0 @@ -package packageupdaters - -import ( - "errors" - "fmt" - biUtils "github.com/jfrog/build-info-go/build/utils" - "github.com/jfrog/frogbot/v3/utils" - "github.com/jfrog/gofrog/version" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/jfrog/jfrog-client-go/utils/log" -) - -const ( - yarnV2Version = "2.0.0" - yarnV1PackageUpdateCmd = "upgrade" - yarnV2PackageUpdateCmd = "up" - modulesFolderFlag = "--modules-folder=" -) - -type YarnPackageUpdater struct { - CommonPackageUpdater -} - -func (yarn *YarnPackageUpdater) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return yarn.updateDirectDependency(vulnDetails) - } else { - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } - } -} - -func (yarn *YarnPackageUpdater) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - isYarn1, executableYarnVersion, err := isYarnV1Project() - if err != nil { - return - } - - var installationCommand string - var extraArgs []string - - if isYarn1 { - installationCommand = yarnV1PackageUpdateCmd - // This dir is created to store node_modules that are created during updating packages in Yarn V1. This dir is to be deleted and not pushed into the PR - var tmpNodeModulesDir string - tmpNodeModulesDir, err = fileutils.CreateTempDir() - defer func() { - err = errors.Join(err, fileutils.RemoveTempDir(tmpNodeModulesDir)) - }() - - if err != nil { - return - } - extraArgs = append(extraArgs, modulesFolderFlag+tmpNodeModulesDir) - } else { - installationCommand = yarnV2PackageUpdateCmd - } - err = yarn.CommonPackageUpdater.UpdateDependency(vulnDetails, installationCommand, extraArgs...) - if err != nil { - err = fmt.Errorf("running 'yarn %s for '%s' failed:\n%s\nHint: The Yarn version that was used is: %s. If your project was built with a different major version of Yarn, please configure your CI runner to include it", - installationCommand, - vulnDetails.ImpactedDependencyName, - err.Error(), - executableYarnVersion) - } - return -} - -// isYarnV1Project gets the current executed yarn version and returns whether the current yarn version is V1 or not -func isYarnV1Project() (isYarn1 bool, executableYarnVersion string, err error) { - // NOTICE: in case your global yarn version is 1.x this function will always return true even if the project is originally in higher yarn version - executableYarnVersion, err = biUtils.GetVersion("yarn", "") - if err != nil { - return - } - log.Info("Using Yarn version: ", executableYarnVersion) - isYarn1 = version.NewVersion(executableYarnVersion).Compare(yarnV2Version) > 0 - return -} diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index 2f2daee07..d89bf920d 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -9,7 +9,7 @@ import ( "regexp" "strings" - "github.com/jfrog/frogbot/v3/packageupdaters" + securitypkgupdaters "github.com/jfrog/jfrog-cli-security/remediation/sca/packageupdaters" "github.com/go-git/go-git/v5" biutils "github.com/jfrog/build-info-go/utils" @@ -54,7 +54,7 @@ type ScanRepositoryCmd struct { baseWd string gitManager *utils.GitManager projectTech []techutils.Technology - updaters map[techutils.Technology]packageupdaters.PackageUpdater + updaters map[techutils.Technology]securitypkgupdaters.PackageUpdater customTemplates utils.CustomTemplates XrayVersion string XscVersion string @@ -285,10 +285,11 @@ func (sr *ScanRepositoryCmd) fixMultiplePackages(vulnerabilities map[string]*uti // else, return the error func (sr *ScanRepositoryCmd) handleUpdatePackageErrors(err error) error { var errUnsupportedFix *utils.ErrUnsupportedFix + var errUnsupportedFixShared *securitypkgupdaters.ErrUnsupportedFix var errNoChangesToCommit *utils.ErrNothingToCommit switch { - case errors.As(err, &errUnsupportedFix): + case errors.As(err, &errUnsupportedFix), errors.As(err, &errUnsupportedFixShared): log.Debug(strings.TrimSpace(err.Error())) case errors.As(err, &errNoChangesToCommit): log.Info(err.Error()) @@ -567,18 +568,35 @@ func (sr *ScanRepositoryCmd) updatePackageToFixedVersion(vulnDetails *utils.Vuln } if sr.updaters == nil { - sr.updaters = make(map[techutils.Technology]packageupdaters.PackageUpdater) - } - - handler := sr.updaters[vulnDetails.Technology] - if handler == nil { - handler = packageupdaters.GetCompatiblePackageUpdater(vulnDetails, sr.scanDetails) + sr.updaters = make(map[techutils.Technology]securitypkgupdaters.PackageUpdater) + } + + fixDetails := &securitypkgupdaters.FixDetails{ + ImpactedDependencyName: vulnDetails.ImpactedDependencyName, + ImpactedDependencyVersion: vulnDetails.ImpactedDependencyVersion, + SuggestedFixedVersion: vulnDetails.SuggestedFixedVersion, + IsDirectDependency: vulnDetails.IsDirectDependency, + Technology: vulnDetails.Technology, + Components: vulnDetails.Components, + IssueId: vulnDetails.IssueId, + } + + handler, cached := sr.updaters[vulnDetails.Technology] + if !cached { + var supported bool + handler, supported = securitypkgupdaters.GetCompatiblePackageUpdater(fixDetails) + if !supported { + log.Debug(fmt.Sprintf("Technology '%s' is not supported for automatic fix — skipping", vulnDetails.Technology)) + sr.updaters[vulnDetails.Technology] = nil // cache nil to skip on next call + return + } sr.updaters[vulnDetails.Technology] = handler - } else if _, unsupported := handler.(*packageupdaters.UnsupportedPackageUpdater); unsupported { + } else if handler == nil { + // Previously determined to be unsupported. return } - return sr.updaters[vulnDetails.Technology].UpdateDependency(vulnDetails) + return handler.UpdateDependency(fixDetails) } // The getRemoteBranchScanHash function extracts the checksum written inside the pull request body and returns it. diff --git a/scanrepository/scanrepository_test.go b/scanrepository/scanrepository_test.go index 6ed441f31..3e93af094 100644 --- a/scanrepository/scanrepository_test.go +++ b/scanrepository/scanrepository_test.go @@ -567,7 +567,7 @@ func TestCreateVulnerabilitiesMap(t *testing.T) { Sca: []violationutils.CveViolation{ { ScaViolation: violationutils.ScaViolation{ - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: "pkg:npm/viol1@1.0.0", PackageURL: "pkg:npm/viol1@1.0.0", }, @@ -580,7 +580,7 @@ func TestCreateVulnerabilitiesMap(t *testing.T) { }, { ScaViolation: violationutils.ScaViolation{ - ImpactedComponent: cyclonedx.Component{ + ImpactedComponent: &cyclonedx.Component{ BOMRef: "pkg:npm/viol2@2.0.0", PackageURL: "pkg:npm/viol2@2.0.0", }, diff --git a/testdata/indirect-projects/go/go.mod.txt b/testdata/indirect-projects/go/go.mod.txt deleted file mode 100755 index eac5f66ae..000000000 --- a/testdata/indirect-projects/go/go.mod.txt +++ /dev/null @@ -1,22 +0,0 @@ -module goproject - -go 1.19 - -require github.com/gin-gonic/gin v1.7.3 - -require ( - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-playground/validator/v10 v10.4.1 // indirect - github.com/golang/protobuf v1.3.3 // indirect - github.com/json-iterator/go v1.1.9 // indirect - github.com/leodido/go-urn v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect - github.com/ugorji/go/codec v1.1.7 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect - golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect -) diff --git a/testdata/indirect-projects/go/go.sum.txt b/testdata/indirect-projects/go/go.sum.txt deleted file mode 100755 index 032aefc80..000000000 --- a/testdata/indirect-projects/go/go.sum.txt +++ /dev/null @@ -1,33 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/testdata/indirect-projects/go/main.go.txt b/testdata/indirect-projects/go/main.go.txt deleted file mode 100755 index 34a990c92..000000000 --- a/testdata/indirect-projects/go/main.go.txt +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - "github.com/gin-gonic/gin" -) - -func main() { - print("test") - _ = gin.Default() -} diff --git a/testdata/indirect-projects/maven/pom.xml b/testdata/indirect-projects/maven/pom.xml deleted file mode 100755 index ba270fc08..000000000 --- a/testdata/indirect-projects/maven/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - 4.0.0 - org.jfrog.test - multi - 3.7-SNAPSHOT - pom - Simple Multi Modules Build - - - UTF-8 - 1.8 - 1.8 - - - - - junit - junit - 3.8.1 - test - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - org.apache.maven.plugins - maven-war-plugin - 2.4 - - - org.apache.maven.plugins - maven-source-plugin - 2.1.2 - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - false - - - - - org.apache.maven.plugins - maven-war-plugin - - - false - - - - - - diff --git a/testdata/indirect-projects/pip/requirements.txt b/testdata/indirect-projects/pip/requirements.txt deleted file mode 100755 index 802474953..000000000 --- a/testdata/indirect-projects/pip/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.21.0 \ No newline at end of file diff --git a/testdata/indirect-projects/pip/setup.py b/testdata/indirect-projects/pip/setup.py deleted file mode 100755 index 3e10635fd..000000000 --- a/testdata/indirect-projects/pip/setup.py +++ /dev/null @@ -1,9 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="pip-example", - version="1.2.3", - install_requires=[ - "requests==2.21.0" - ], -) diff --git a/testdata/indirect-projects/pipenv/Pipfile b/testdata/indirect-projects/pipenv/Pipfile deleted file mode 100755 index 6d367636b..000000000 --- a/testdata/indirect-projects/pipenv/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests = "==2.21.0" - -[dev-packages] - -[requires] -python_version = "*" diff --git a/testdata/indirect-projects/pipenv/Pipfile.lock b/testdata/indirect-projects/pipenv/Pipfile.lock deleted file mode 100755 index 120702377..000000000 --- a/testdata/indirect-projects/pipenv/Pipfile.lock +++ /dev/null @@ -1,59 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "558fa3d8925a85d24792479c399941b74eda5d07bde65eace645285c07547361" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "*" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.12.7" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "requests": { - "hashes": [ - "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", - "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "urllib3": { - "hashes": [ - "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", - "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", - "version": "==1.24.3" - } - }, - "develop": {} -} diff --git a/testdata/indirect-projects/poetry/pyproject.toml b/testdata/indirect-projects/poetry/pyproject.toml deleted file mode 100755 index 4696f1574..000000000 --- a/testdata/indirect-projects/poetry/pyproject.toml +++ /dev/null @@ -1,13 +0,0 @@ -[tool.poetry] -name = "poetry-project" -version = "0.1.0" -description = "" -authors = ["Your Name "] - -[tool.poetry.dependencies] -python = "^3.10" -requests = "==2.21.0" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" diff --git a/testdata/packageupdaters/pom.xml b/testdata/packageupdaters/pom.xml deleted file mode 100755 index a8bf25093..000000000 --- a/testdata/packageupdaters/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - 4.0.0 - - org.jenkins-ci.plugins - plugin - 4.53 - - - - 2.361 - true - 2.37.2 - - io.jenkins.plugins - jfrog - 1.1.x-SNAPSHOT - hpi - JFrog Plugin - - - - org.jfrog.filespecs - file-specs-java - 1.1.1 - - - - - - com.fasterxml.jackson.core - jackson-core - 2.13.4 - - - org.apache.httpcomponents - httpcore - 4.4.15 - - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - repo.jenkins-ci.org.releases - https://repo.jenkins-ci.org/releases/ - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - repo.jenkins-ci.org.releases - https://repo.jenkins-ci.org/releases/ - - - diff --git a/testdata/projects/npm-rollback/package-lock.json b/testdata/projects/npm-rollback/package-lock.json deleted file mode 100644 index d605327db..000000000 --- a/testdata/projects/npm-rollback/package-lock.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "npm-rollback-test", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "npm-rollback-test", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "minimist": "1.2.5", - "this-package-does-not-exist-frogbot-test": "1.0.0" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "license": "MIT" - } - } -} - diff --git a/testdata/projects/npm-rollback/package.json b/testdata/projects/npm-rollback/package.json deleted file mode 100644 index 8cbb19efa..000000000 --- a/testdata/projects/npm-rollback/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "npm-rollback-test", - "version": "1.0.0", - "description": "Test project for rollback functionality", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "minimist": "1.2.5", - "this-package-does-not-exist-frogbot-test": "1.0.0" - } -} - diff --git a/testdata/projects/npm/package-lock.json b/testdata/projects/npm/package-lock.json deleted file mode 100644 index dfb2a025a..000000000 --- a/testdata/projects/npm/package-lock.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "npm", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "npm", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "minimist": "1.2.5" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://entplus.jfrog.io/artifactory/api/npm/npm-virtual/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "license": "MIT" - } - } -}