diff --git a/packagehandlers/commonpackagehandler.go b/packagehandlers/commonpackagehandler.go index 887a2a210..12cfbaddf 100644 --- a/packagehandlers/commonpackagehandler.go +++ b/packagehandlers/commonpackagehandler.go @@ -35,7 +35,7 @@ func GetCompatiblePackageHandler(vulnDetails *utils.VulnerabilityDetails, detail case techutils.Pip: handler = &PythonPackageHandler{pipRequirementsFile: defaultRequirementFile} case techutils.Maven: - handler = NewMavenPackageHandler(details) + handler = &MavenPackageUpdater{} case techutils.Nuget: handler = &NugetPackageHandler{} case techutils.Gradle: diff --git a/packagehandlers/mavenpackagehandler.go b/packagehandlers/mavenpackagehandler.go deleted file mode 100644 index f5bb70f69..000000000 --- a/packagehandlers/mavenpackagehandler.go +++ /dev/null @@ -1,280 +0,0 @@ -package packagehandlers - -import ( - "encoding/json" - "encoding/xml" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/java" - "github.com/jfrog/jfrog-client-go/utils/log" - "golang.org/x/exp/slices" - - "github.com/jfrog/frogbot/v2/utils" -) - -const MavenVersionNotAvailableErrorFormat = "Version %s is not available for artifact" - -type gavCoordinate struct { - GroupId string `xml:"groupId"` - ArtifactId string `xml:"artifactId"` - Version string `xml:"version"` - foundInDependencyManagement bool -} - -func (gc *gavCoordinate) isEmpty() bool { - return gc.GroupId == "" && gc.ArtifactId == "" && gc.Version == "" -} - -func (gc *gavCoordinate) trimSpaces() *gavCoordinate { - gc.GroupId = strings.TrimSpace(gc.GroupId) - gc.ArtifactId = strings.TrimSpace(gc.ArtifactId) - gc.Version = strings.TrimSpace(gc.Version) - return gc -} - -type mavenDependency struct { - gavCoordinate - Dependencies []mavenDependency `xml:"dependencies>dependency"` - DependencyManagement []mavenDependency `xml:"dependencyManagement>dependencies>dependency"` - Plugins []mavenPlugin `xml:"build>plugins>plugin"` -} - -func (md *mavenDependency) collectMavenDependencies(foundInDependencyManagement bool) []gavCoordinate { - var result []gavCoordinate - if !md.isEmpty() { - md.foundInDependencyManagement = foundInDependencyManagement - result = append(result, *md.trimSpaces()) - } - for _, dependency := range md.Dependencies { - result = append(result, dependency.collectMavenDependencies(foundInDependencyManagement)...) - } - for _, dependency := range md.DependencyManagement { - result = append(result, dependency.collectMavenDependencies(true)...) - } - for _, plugin := range md.Plugins { - result = append(result, plugin.collectMavenPlugins()...) - } - - return result -} - -type mavenPlugin struct { - gavCoordinate - NestedPlugins []mavenPlugin `xml:"configuration>plugins>plugin"` -} - -func (mp *mavenPlugin) collectMavenPlugins() []gavCoordinate { - var result []gavCoordinate - if !mp.isEmpty() { - result = append(result, *mp.trimSpaces()) - } - for _, plugin := range mp.NestedPlugins { - result = append(result, plugin.collectMavenPlugins()...) - } - return result -} - -// fillDependenciesMap collects direct dependencies from the pomPath pom.xml file. -// If the version of a dependency is set in another property section, it is added as its value in the map. -func (mph *MavenPackageHandler) fillDependenciesMap(pomPath string) error { - contentBytes, err := os.ReadFile(filepath.Clean(pomPath)) - if err != nil { - return errors.New("couldn't read pom.xml file: " + err.Error()) - } - mavenDependencies, err := getMavenDependencies(contentBytes) - if err != nil { - return err - } - for _, dependency := range mavenDependencies { - if dependency.Version == "" { - continue - } - depName := fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId) - if _, exist := mph.pomDependencies[depName]; !exist { - mph.pomDependencies[depName] = pomDependencyDetails{foundInDependencyManagement: dependency.foundInDependencyManagement, currentVersion: dependency.Version} - } - if strings.HasPrefix(dependency.Version, "${") { - trimmedVersion := strings.Trim(dependency.Version, "${}") - if !slices.Contains(mph.pomDependencies[depName].properties, trimmedVersion) { - mph.pomDependencies[depName] = pomDependencyDetails{ - properties: append(mph.pomDependencies[depName].properties, trimmedVersion), - currentVersion: dependency.Version, - foundInDependencyManagement: dependency.foundInDependencyManagement, - } - } - } - } - return nil -} - -// Extract all dependencies from the input pom.xml -// pomXmlContent - The pom.xml content -func getMavenDependencies(pomXmlContent []byte) (result []gavCoordinate, err error) { - var dependencies mavenDependency - if err = xml.Unmarshal(pomXmlContent, &dependencies); err != nil { - err = fmt.Errorf("failed to unmarshal the current pom.xml:\n%s, error received:\n%w"+string(pomXmlContent), err) - return - } - result = append(result, dependencies.collectMavenDependencies(false)...) - return -} - -type pomPath struct { - PomPath string `json:"pomPath"` -} - -type pomDependencyDetails struct { - properties []string - currentVersion string - foundInDependencyManagement bool -} - -func NewMavenPackageHandler(scanDetails *utils.ScanDetails) *MavenPackageHandler { - depTreeParams := &java.DepTreeParams{ - Server: scanDetails.ServerDetails, - IsMavenDepTreeInstalled: true, - } - // The mvn-dep-tree plugin has already been installed during the audit dependency tree build phase, - // Therefore, we set the `isDepTreeInstalled` flag to true - mavenDepTreeManager := java.NewMavenDepTreeManager(depTreeParams, java.Projects) - return &MavenPackageHandler{MavenDepTreeManager: mavenDepTreeManager} -} - -type MavenPackageHandler struct { - CommonPackageHandler - // pomDependencies holds a map of direct dependencies found in pom.xml. - pomDependencies map[string]pomDependencyDetails - // pomPaths holds the paths to all the pom.xml files that are related to the current project. - pomPaths []pomPath - // mavenDepTreeManager handles the installation and execution of the maven-dep-tree to obtain all the project poms and running mvn commands - *java.MavenDepTreeManager -} - -func (mph *MavenPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - // When resolution from an Artifactory server is necessary, a settings.xml file will be generated, and its path will be set in mph. - if mph.GetDepsRepo() != "" { - var clearMavenDepTreeRun func() error - _, clearMavenDepTreeRun, err = mph.CreateTempDirWithSettingsXmlIfNeeded() - if err != nil { - return - } - defer func() { - err = errors.Join(err, clearMavenDepTreeRun()) - }() - } - - err = mph.getProjectPoms() - if err != nil { - return err - } - - // Get direct dependencies for each pom.xml file - if mph.pomDependencies == nil { - mph.pomDependencies = make(map[string]pomDependencyDetails) - } - for _, pp := range mph.pomPaths { - if err = mph.fillDependenciesMap(pp.PomPath); err != nil { - return err - } - } - - var depDetails pomDependencyDetails - var exists bool - // Check if the impacted package is a direct dependency - impactedDependency := vulnDetails.ImpactedDependencyName - if depDetails, exists = mph.pomDependencies[impactedDependency]; !exists { - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } - } - if len(depDetails.properties) > 0 { - return mph.updateProperties(&depDetails, vulnDetails.SuggestedFixedVersion) - } - - return mph.updatePackageVersion(vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, depDetails.foundInDependencyManagement) -} - -// Returns project's Pom paths. This function requires an execution of maven-dep-tree 'project' command prior to its execution -func (mph *MavenPackageHandler) getProjectPoms() (err error) { - // Check if we already scanned the project pom.xml locations - if len(mph.pomPaths) > 0 { - return - } - - oldSettingsXmlPath := mph.GetSettingsXmlPath() - - var depTreeOutput string - var clearMavenDepTreeRun func() error - if depTreeOutput, clearMavenDepTreeRun, err = mph.RunMavenDepTree(); err != nil { - err = fmt.Errorf("failed to get project poms while running maven-dep-tree: %s", err.Error()) - if clearMavenDepTreeRun != nil { - err = errors.Join(err, clearMavenDepTreeRun()) - } - return - } - defer func() { - err = clearMavenDepTreeRun() - mph.SetSettingsXmlPath(oldSettingsXmlPath) - }() - - for _, jsonContent := range strings.Split(depTreeOutput, "\n") { - if jsonContent == "" { - continue - } - // Escape backslashes in the pomPath field, to fix windows backslash parsing issues - escapedContent := strings.ReplaceAll(jsonContent, `\`, `\\`) - var pp pomPath - if err = json.Unmarshal([]byte(escapedContent), &pp); err != nil { - err = fmt.Errorf("failed to unmarshal the maven-dep-tree output. Full maven-dep-tree output:\n%s\nCurrent line:\n%s\nError details:\n%w", depTreeOutput, escapedContent, err) - return - } - mph.pomPaths = append(mph.pomPaths, pp) - } - if len(mph.pomPaths) == 0 { - err = errors.New("couldn't find any pom.xml files in the current project") - } - return -} - -// Update the package version. Updates it only if the version is not a reference to a property. -func (mph *MavenPackageHandler) updatePackageVersion(impactedPackage, fixedVersion string, foundInDependencyManagement bool) error { - updateVersionArgs := []string{ - "-U", "-B", "org.codehaus.mojo:versions-maven-plugin:use-dep-version", "-Dincludes=" + impactedPackage, - "-DdepVersion=" + fixedVersion, "-DgenerateBackupPoms=false", - fmt.Sprintf("-DprocessDependencies=%t", !foundInDependencyManagement), - fmt.Sprintf("-DprocessDependencyManagement=%t", foundInDependencyManagement)} - updateVersionCmd := fmt.Sprintf("mvn %s", strings.Join(updateVersionArgs, " ")) - log.Debug(fmt.Sprintf("Running '%s'", updateVersionCmd)) - output, err := mph.RunMvnCmd(updateVersionArgs) - if err != nil { - versionNotAvailableString := fmt.Sprintf(MavenVersionNotAvailableErrorFormat, fixedVersion) - // Replace Maven's 'version not available' error with more readable error message - if strings.Contains(string(output), versionNotAvailableString) { - err = fmt.Errorf("couldn't update %q to suggested fix version: %s", impactedPackage, versionNotAvailableString) - } - } - return err -} - -// Update properties that represent this package's version. -func (mph *MavenPackageHandler) updateProperties(depDetails *pomDependencyDetails, fixedVersion string) error { - for _, property := range depDetails.properties { - updatePropertyArgs := []string{ - "-U", "-B", "org.codehaus.mojo:versions-maven-plugin:set-property", "-Dproperty=" + property, - "-DnewVersion=" + fixedVersion, "-DgenerateBackupPoms=false", - fmt.Sprintf("-DprocessDependencies=%t", !depDetails.foundInDependencyManagement), - fmt.Sprintf("-DprocessDependencyManagement=%t", depDetails.foundInDependencyManagement)} - updatePropertyCmd := fmt.Sprintf("mvn %s", strings.Join(updatePropertyArgs, " ")) - log.Debug(fmt.Sprintf("Running '%s'", updatePropertyCmd)) - if _, err := mph.RunMvnCmd(updatePropertyArgs); err != nil { // #nosec G204 - return fmt.Errorf("failed updating %s property: %s", property, err.Error()) - } - } - return nil -} diff --git a/packagehandlers/mavenpackageupdater.go b/packagehandlers/mavenpackageupdater.go new file mode 100644 index 000000000..31f4b712d --- /dev/null +++ b/packagehandlers/mavenpackageupdater.go @@ -0,0 +1,207 @@ +package packagehandlers + +import ( + "bytes" + "encoding/xml" + "fmt" + "os" + "regexp" + "strings" + + "github.com/jfrog/frogbot/v2/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +const ( + mavenCoordinateSeparator = ":" + propertyPrefix = "${" + propertySuffix = "}" +) + +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 (mpu *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 := mpu.getPomPaths(vulnDetails) + if len(pomPaths) == 0 { + return fmt.Errorf("no pom.xml locations found for %s - Components array is empty or missing Location data", vulnDetails.ImpactedDependencyName) + } + + var errors []string + for _, pomPath := range pomPaths { + if err := mpu.updatePomFile(pomPath, groupId, artifactId, vulnDetails.SuggestedFixedVersion); err != nil { + errors = append(errors, fmt.Sprintf("%s: %v", pomPath, err)) + } + } + + if len(errors) > 0 { + return fmt.Errorf("failed to update pom.xml files:\n%s", strings.Join(errors, "\n")) + } + return nil +} + +func (mpu *MavenPackageUpdater) getPomPaths(vulnDetails *utils.VulnerabilityDetails) []string { + var pomPaths []string + for _, component := range vulnDetails.Components { + if component.Location != nil && component.Location.File != "" { + pomPaths = append(pomPaths, component.Location.File) + } + } + return pomPaths +} + +func (mpu *MavenPackageUpdater) updatePomFile(pomPath, groupId, artifactId, fixedVersion string) error { + 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) + } + + updated := false + newContent := content + + if updated, newContent = mpu.updateInParent(&project, groupId, artifactId, fixedVersion, newContent); updated { + if err := os.WriteFile(pomPath, newContent, 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", pomPath, err) + } + log.Debug("Successfully updated", pomPath) + return nil + } + + if updated, newContent = mpu.updateInDependencies(&project, project.Dependencies, groupId, artifactId, fixedVersion, newContent); updated { + if err := os.WriteFile(pomPath, newContent, 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", pomPath, err) + } + log.Debug("Successfully updated", pomPath) + return nil + } + + if project.DependencyManagement != nil { + if updated, newContent = mpu.updateInDependencies(&project, project.DependencyManagement.Dependencies, groupId, artifactId, fixedVersion, newContent); updated { + if err := os.WriteFile(pomPath, newContent, 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", pomPath, err) + } + log.Debug("Successfully updated", pomPath) + return nil + } + } + + return fmt.Errorf("dependency %s not found in %s", toDependencyName(groupId, artifactId), pomPath) +} + +func (mpu *MavenPackageUpdater) SetCommonParams(serverDetails *config.ServerDetails, depsRepo string) {} + +func parseDependencyName(dependencyName string) (groupId, artifactId string, err error) { + parts := strings.Split(dependencyName, mavenCoordinateSeparator) + 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 + mavenCoordinateSeparator + artifactId +} + +func (mpu *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 (mpu *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 mpu.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 (mpu *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) { + log.Debug("Updated property", propertyName, "to", newValue) + return true, newContent + } + } + } + return false, content +} diff --git a/packagehandlers/packagehandlers_test.go b/packagehandlers/packagehandlers_test.go index 0034c29a0..4fc3fdbc5 100644 --- a/packagehandlers/packagehandlers_test.go +++ b/packagehandlers/packagehandlers_test.go @@ -1,6 +1,7 @@ package packagehandlers import ( + "errors" "fmt" "os" "path/filepath" @@ -12,7 +13,6 @@ import ( "github.com/jfrog/build-info-go/tests" biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/frogbot/v2/utils" - "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/java" "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" @@ -406,275 +406,262 @@ func TestPipPackageRegex(t *testing.T) { } } -// Maven utils functions -func TestGetDependenciesFromPomXmlSingleDependency(t *testing.T) { - testCases := []string{` - org.apache.commons - commons-email - 1.1 - compile -`, - ` - org.apache.commons - commons-email - 1.1 - compile -`, - } +func TestMavenUpdateRegularDependency(t *testing.T) { + testProjectPath := filepath.Join("..", "testdata", "packagehandlers") + currDir, err := os.Getwd() + assert.NoError(t, err) + tmpDir, err := os.MkdirTemp("", "maven-test-*") + assert.NoError(t, err) + defer func() { + assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) + }() - for _, testCase := range testCases { - result, err := getMavenDependencies([]byte(testCase)) - assert.NoError(t, err) + assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil)) + assert.NoError(t, os.Chdir(tmpDir)) + defer func() { + assert.NoError(t, os.Chdir(currDir)) + }() - assert.Len(t, result, 1) - assert.Equal(t, "org.apache.commons", result[0].GroupId) - assert.Equal(t, "commons-email", result[0].ArtifactId) - assert.Equal(t, "1.1", result[0].Version) + updater := &MavenPackageUpdater{} + 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{ + {Location: &formats.Location{File: "pom.xml"}}, + }, + }, + }, } -} -func TestGetDependenciesFromPomXmlMultiDependency(t *testing.T) { - testCases := []string{` - - - - org.apache.commons - commons-email - 1.1 - compile - - - org.codehaus.plexus - plexus-utils - 1.5.1 - - -`, - } + err = updater.UpdateDependency(vulnDetails) + assert.NoError(t, err) - for _, testCase := range testCases { - result, err := getMavenDependencies([]byte(testCase)) - assert.NoError(t, err) + modifiedPom, err := os.ReadFile("pom.xml") + assert.NoError(t, err) + assert.Contains(t, string(modifiedPom), "1.1.5") + assert.NotContains(t, string(modifiedPom), "1.1.1") +} - assert.Len(t, result, 2) - assert.Equal(t, "org.apache.commons", result[0].GroupId) - assert.Equal(t, "commons-email", result[0].ArtifactId) - assert.Equal(t, "1.1", result[0].Version) +func TestMavenUpdateDependencyManagement(t *testing.T) { + testProjectPath := filepath.Join("..", "testdata", "packagehandlers") + currDir, err := os.Getwd() + assert.NoError(t, err) + 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)) + }() - assert.Equal(t, "org.codehaus.plexus", result[1].GroupId) - assert.Equal(t, "plexus-utils", result[1].ArtifactId) - assert.Equal(t, "1.5.1", result[1].Version) + updater := &MavenPackageUpdater{} + 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{ + {Location: &formats.Location{File: "pom.xml"}}, + }, + }, + }, } -} -func TestGetPluginsFromPomXml(t *testing.T) { - testCase := - ` - - - - org.apache.maven.plugins - maven-source-plugin - - - com.github.spotbugs - spotbugs-maven-plugin - 4.5.3.0 - - spotbugs-security-exclude.xml - - - com.h3xstream.findsecbugs - findsecbugs-plugin - 1.12.0 - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.1 - - - - true - - - **/InjectedTest.java - **/*ITest.java - - - - - - - ` - plugins, err := getMavenDependencies([]byte(testCase)) + err = updater.UpdateDependency(vulnDetails) assert.NoError(t, err) - assert.Equal(t, "org.apache.maven.plugins", plugins[0].GroupId) - assert.Equal(t, "maven-source-plugin", plugins[0].ArtifactId) - assert.Equal(t, "com.github.spotbugs", plugins[1].GroupId) - assert.Equal(t, "spotbugs-maven-plugin", plugins[1].ArtifactId) - assert.Equal(t, "4.5.3.0", plugins[1].Version) - assert.Equal(t, "com.h3xstream.findsecbugs", plugins[2].GroupId) - assert.Equal(t, "findsecbugs-plugin", plugins[2].ArtifactId) - assert.Equal(t, "1.12.0", plugins[2].Version) - assert.Equal(t, "org.apache.maven.plugins", plugins[3].GroupId) - assert.Equal(t, "maven-surefire-plugin", plugins[3].ArtifactId) - assert.Equal(t, "2.22.1", plugins[3].Version) -} -func TestGetDependenciesFromDependencyManagement(t *testing.T) { - testCase := ` - - - - - io.jenkins.tools.bom - bom-2.346.x - 1607.va_c1576527071 - import - pom - - - com.fasterxml.jackson.core - jackson-core - 2.13.4 - - - com.fasterxml.jackson.core - jackson-databind - 2.13.4.2 - - - com.fasterxml.jackson.core - jackson-annotations - 2.13.4 - - - org.apache.httpcomponents - httpcore - 4.4.15 - - - org.jenkins-ci.plugins.workflow - workflow-durable-task-step - 1190.vc93d7d457042 - test - - - - -` - dependencies, err := getMavenDependencies([]byte(testCase)) + modifiedPom, err := os.ReadFile("pom.xml") assert.NoError(t, err) - assert.Len(t, dependencies, 6) - for _, dependency := range dependencies { - assert.True(t, dependency.foundInDependencyManagement) - } + assert.Contains(t, string(modifiedPom), "2.15.0") + assert.NotContains(t, string(modifiedPom), "2.13.4") } -func TestGetProjectPoms(t *testing.T) { - mvnHandler := &MavenPackageHandler{MavenDepTreeManager: java.NewMavenDepTreeManager(&java.DepTreeParams{IsMavenDepTreeInstalled: false}, java.Projects)} +func TestMavenUpdatePropertyVersion(t *testing.T) { + testProjectPath := filepath.Join("..", "testdata", "packagehandlers") currDir, err := os.Getwd() assert.NoError(t, err) - tmpDir, err := os.MkdirTemp("", "") + tmpDir, err := os.MkdirTemp("", "maven-test-*") + assert.NoError(t, err) defer func() { assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) }() - assert.NoError(t, err) - assert.NoError(t, biutils.CopyDir(filepath.Join("..", "testdata", "projects", "maven"), tmpDir, true, nil)) + + assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil)) + + pomContent := ` + + 4.0.0 + test + test + 1.0 + + + 2.9.8 + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + +` + + assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "pom.xml"), []byte(pomContent), 0644)) assert.NoError(t, os.Chdir(tmpDir)) defer func() { assert.NoError(t, os.Chdir(currDir)) }() - assert.NoError(t, mvnHandler.getProjectPoms()) - assert.Len(t, mvnHandler.pomPaths, 2) -} - -// General Utils functions -func TestFixVersionInfo_UpdateFixVersionIfMax(t *testing.T) { - type testCase struct { - fixVersionInfo utils.VulnerabilityDetails - newFixVersion string - expectedOutput string + updater := &MavenPackageUpdater{} + 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{ + {Location: &formats.Location{File: "pom.xml"}}, + }, + }, + }, } - testCases := []testCase{ - {fixVersionInfo: utils.VulnerabilityDetails{SuggestedFixedVersion: "1.2.3", IsDirectDependency: true}, newFixVersion: "1.2.4", expectedOutput: "1.2.4"}, - {fixVersionInfo: utils.VulnerabilityDetails{SuggestedFixedVersion: "1.2.3", IsDirectDependency: true}, newFixVersion: "1.0.4", expectedOutput: "1.2.3"}, - } + err = updater.UpdateDependency(vulnDetails) + assert.NoError(t, err) - for _, tc := range testCases { - t.Run(tc.expectedOutput, func(t *testing.T) { - tc.fixVersionInfo.UpdateFixVersionIfMax(tc.newFixVersion) - assert.Equal(t, tc.expectedOutput, tc.fixVersionInfo.SuggestedFixedVersion) - }) - } + modifiedPom, err := os.ReadFile("pom.xml") + assert.NoError(t, err) + assert.Contains(t, string(modifiedPom), "2.13.0") + assert.NotContains(t, string(modifiedPom), "2.9.8") } -func TestUpdatePackageVersion(t *testing.T) { +func TestMavenUpdateParentPOM(t *testing.T) { testProjectPath := filepath.Join("..", "testdata", "packagehandlers") currDir, err := os.Getwd() assert.NoError(t, err) - tmpDir, err := os.MkdirTemp("", "") + tmpDir, err := os.MkdirTemp("", "maven-test-*") + assert.NoError(t, err) defer func() { assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) }() - assert.NoError(t, err) + assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil)) + + pomContent := ` + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.0 + + + test + test + 1.0 +` + + assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "pom.xml"), []byte(pomContent), 0644)) assert.NoError(t, os.Chdir(tmpDir)) defer func() { assert.NoError(t, os.Chdir(currDir)) }() - testCases := []struct { - impactedPackage string - fixedVersion string - foundInDependencyManagement bool - }{ - {impactedPackage: "org.jfrog.filespecs:file-specs-java", fixedVersion: "1.1.2"}, - {impactedPackage: "com.fasterxml.jackson.core:jackson-core", fixedVersion: "2.15.0", foundInDependencyManagement: true}, - {impactedPackage: "org.apache.httpcomponents:httpcore", fixedVersion: "4.4.16", foundInDependencyManagement: true}, - } - mvnHandler := &MavenPackageHandler{MavenDepTreeManager: &java.MavenDepTreeManager{}} - for _, test := range testCases { - assert.NoError(t, mvnHandler.updatePackageVersion(test.impactedPackage, test.fixedVersion, test.foundInDependencyManagement)) + + updater := &MavenPackageUpdater{} + 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{ + {Location: &formats.Location{File: "pom.xml"}}, + }, + }, + }, } - modifiedPom, err := os.ReadFile("pom.xml") + + err = updater.UpdateDependency(vulnDetails) assert.NoError(t, err) - for _, test := range testCases { - assert.Contains(t, fmt.Sprintf("%s", string(modifiedPom)), test.fixedVersion) - } - // Test non-existing version error - assert.ErrorContains(t, - mvnHandler.updatePackageVersion("org.apache.httpcomponents:httpcore", "non.existing.version", true), - fmt.Sprintf(MavenVersionNotAvailableErrorFormat, "non.existing.version")) + modifiedPom, err := os.ReadFile("pom.xml") + assert.NoError(t, err) + assert.Contains(t, string(modifiedPom), "2.7.0") + assert.NotContains(t, string(modifiedPom), "2.5.0") } -func TestUpdatePropertiesVersion(t *testing.T) { +func TestMavenDependencyNotFound(t *testing.T) { testProjectPath := filepath.Join("..", "testdata", "packagehandlers") currDir, err := os.Getwd() assert.NoError(t, err) - tmpDir, err := os.MkdirTemp("", "") + tmpDir, err := os.MkdirTemp("", "maven-test-*") + assert.NoError(t, err) defer func() { assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) }() - assert.NoError(t, err) + assert.NoError(t, biutils.CopyDir(testProjectPath, tmpDir, true, nil)) assert.NoError(t, os.Chdir(tmpDir)) defer func() { assert.NoError(t, os.Chdir(currDir)) }() - mvnHandler := &MavenPackageHandler{MavenDepTreeManager: &java.MavenDepTreeManager{}} - assert.NoError(t, mvnHandler.updateProperties(&pomDependencyDetails{properties: []string{"buildinfo.version"}}, "2.39.9")) - modifiedPom, err := os.ReadFile("pom.xml") - assert.NoError(t, err) - assert.Contains(t, string(modifiedPom), "2.39.9") + + updater := &MavenPackageUpdater{} + 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{ + {Location: &formats.Location{File: "pom.xml"}}, + }, + }, + }, + } + + err = updater.UpdateDependency(vulnDetails) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestMavenIndirectDependencyNotSupported(t *testing.T) { + updater := &MavenPackageUpdater{} + 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{ + {Location: &formats.Location{File: "pom.xml"}}, + }, + }, + }, + } + + err := updater.UpdateDependency(vulnDetails) + assert.Error(t, err) + + var unsupportedErr *utils.ErrUnsupportedFix + assert.True(t, errors.As(err, &unsupportedErr)) + assert.Equal(t, utils.IndirectDependencyFixNotSupported, unsupportedErr.ErrorType) } func getTestDataDir(t *testing.T, directDependency bool) string { diff --git a/utils/utils.go b/utils/utils.go index e9225be70..95ba43b13 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -93,12 +93,9 @@ func (err *ErrNothingToCommit) Error() string { // VulnerabilityDetails serves as a container for essential information regarding a vulnerability that is going to be addressed and resolved type VulnerabilityDetails struct { formats.VulnerabilityOrViolationRow - // Suggested fix version SuggestedFixedVersion string - // States whether the dependency is direct or transitive - IsDirectDependency bool - // Cves as a list of string - Cves []string + IsDirectDependency bool + Cves []string } func NewVulnerabilityDetails(vulnerability formats.VulnerabilityOrViolationRow, fixVersion string) *VulnerabilityDetails {