github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/packagehandlers/mavenpackagehandler.go (about) 1 package packagehandlers 2 3 import ( 4 "encoding/json" 5 "encoding/xml" 6 "errors" 7 "fmt" 8 "github.com/jfrog/frogbot/utils" 9 "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/java" 10 "github.com/jfrog/jfrog-client-go/utils/log" 11 "golang.org/x/exp/slices" 12 "os" 13 "path/filepath" 14 "strings" 15 ) 16 17 const MavenVersionNotAvailableErrorFormat = "Version %s is not available for artifact" 18 19 type gavCoordinate struct { 20 GroupId string `xml:"groupId"` 21 ArtifactId string `xml:"artifactId"` 22 Version string `xml:"version"` 23 foundInDependencyManagement bool 24 } 25 26 func (gc *gavCoordinate) isEmpty() bool { 27 return gc.GroupId == "" && gc.ArtifactId == "" && gc.Version == "" 28 } 29 30 func (gc *gavCoordinate) trimSpaces() *gavCoordinate { 31 gc.GroupId = strings.TrimSpace(gc.GroupId) 32 gc.ArtifactId = strings.TrimSpace(gc.ArtifactId) 33 gc.Version = strings.TrimSpace(gc.Version) 34 return gc 35 } 36 37 type mavenDependency struct { 38 gavCoordinate 39 Dependencies []mavenDependency `xml:"dependencies>dependency"` 40 DependencyManagement []mavenDependency `xml:"dependencyManagement>dependencies>dependency"` 41 Plugins []mavenPlugin `xml:"build>plugins>plugin"` 42 } 43 44 func (md *mavenDependency) collectMavenDependencies(foundInDependencyManagement bool) []gavCoordinate { 45 var result []gavCoordinate 46 if !md.isEmpty() { 47 md.foundInDependencyManagement = foundInDependencyManagement 48 result = append(result, *md.trimSpaces()) 49 } 50 for _, dependency := range md.Dependencies { 51 result = append(result, dependency.collectMavenDependencies(foundInDependencyManagement)...) 52 } 53 for _, dependency := range md.DependencyManagement { 54 result = append(result, dependency.collectMavenDependencies(true)...) 55 } 56 for _, plugin := range md.Plugins { 57 result = append(result, plugin.collectMavenPlugins()...) 58 } 59 60 return result 61 } 62 63 type mavenPlugin struct { 64 gavCoordinate 65 NestedPlugins []mavenPlugin `xml:"configuration>plugins>plugin"` 66 } 67 68 func (mp *mavenPlugin) collectMavenPlugins() []gavCoordinate { 69 var result []gavCoordinate 70 if !mp.isEmpty() { 71 result = append(result, *mp.trimSpaces()) 72 } 73 for _, plugin := range mp.NestedPlugins { 74 result = append(result, plugin.collectMavenPlugins()...) 75 } 76 return result 77 } 78 79 // fillDependenciesMap collects direct dependencies from the pomPath pom.xml file. 80 // If the version of a dependency is set in another property section, it is added as its value in the map. 81 func (mph *MavenPackageHandler) fillDependenciesMap(pomPath string) error { 82 contentBytes, err := os.ReadFile(filepath.Clean(pomPath)) 83 if err != nil { 84 return errors.New("couldn't read pom.xml file: " + err.Error()) 85 } 86 mavenDependencies, err := getMavenDependencies(contentBytes) 87 if err != nil { 88 return err 89 } 90 for _, dependency := range mavenDependencies { 91 if dependency.Version == "" { 92 continue 93 } 94 depName := fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId) 95 if _, exist := mph.pomDependencies[depName]; !exist { 96 mph.pomDependencies[depName] = pomDependencyDetails{foundInDependencyManagement: dependency.foundInDependencyManagement, currentVersion: dependency.Version} 97 } 98 if strings.HasPrefix(dependency.Version, "${") { 99 trimmedVersion := strings.Trim(dependency.Version, "${}") 100 if !slices.Contains(mph.pomDependencies[depName].properties, trimmedVersion) { 101 mph.pomDependencies[depName] = pomDependencyDetails{ 102 properties: append(mph.pomDependencies[depName].properties, trimmedVersion), 103 currentVersion: dependency.Version, 104 foundInDependencyManagement: dependency.foundInDependencyManagement, 105 } 106 } 107 } 108 } 109 return nil 110 } 111 112 // Extract all dependencies from the input pom.xml 113 // pomXmlContent - The pom.xml content 114 func getMavenDependencies(pomXmlContent []byte) (result []gavCoordinate, err error) { 115 var dependencies mavenDependency 116 if err = xml.Unmarshal(pomXmlContent, &dependencies); err != nil { 117 err = fmt.Errorf("failed to unmarshal the current pom.xml:\n%s, error received:\n%w"+string(pomXmlContent), err) 118 return 119 } 120 result = append(result, dependencies.collectMavenDependencies(false)...) 121 return 122 } 123 124 type pomPath struct { 125 PomPath string `json:"pomPath"` 126 } 127 128 type pomDependencyDetails struct { 129 properties []string 130 currentVersion string 131 foundInDependencyManagement bool 132 } 133 134 func NewMavenPackageHandler(scanDetails *utils.ScanDetails) *MavenPackageHandler { 135 depTreeParams := &java.DepTreeParams{ 136 Server: scanDetails.ServerDetails, 137 DepsRepo: scanDetails.DepsRepo, 138 } 139 // The mvn-dep-tree plugin has already been installed during the audit dependency tree build phase, 140 // Therefore, we set the `isDepTreeInstalled` flag to true 141 mavenDepTreeManager := java.NewMavenDepTreeManager(depTreeParams, java.Projects, true) 142 return &MavenPackageHandler{MavenDepTreeManager: mavenDepTreeManager} 143 } 144 145 type MavenPackageHandler struct { 146 CommonPackageHandler 147 // pomDependencies holds a map of direct dependencies found in pom.xml. 148 pomDependencies map[string]pomDependencyDetails 149 // pomPaths holds the paths to all the pom.xml files that are related to the current project. 150 pomPaths []pomPath 151 // mavenDepTreeManager handles the installation and execution of the maven-dep-tree to obtain all the project poms and running mvn commands 152 *java.MavenDepTreeManager 153 } 154 155 func (mph *MavenPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { 156 if err := mph.getProjectPoms(); err != nil { 157 return err 158 } 159 // Get direct dependencies for each pom.xml file 160 if mph.pomDependencies == nil { 161 mph.pomDependencies = make(map[string]pomDependencyDetails) 162 } 163 for _, pp := range mph.pomPaths { 164 if err := mph.fillDependenciesMap(pp.PomPath); err != nil { 165 return err 166 } 167 } 168 169 var depDetails pomDependencyDetails 170 var exists bool 171 // Check if the impacted package is a direct dependency 172 impactedDependency := vulnDetails.ImpactedDependencyName 173 if depDetails, exists = mph.pomDependencies[impactedDependency]; !exists { 174 return &utils.ErrUnsupportedFix{ 175 PackageName: vulnDetails.ImpactedDependencyName, 176 FixedVersion: vulnDetails.SuggestedFixedVersion, 177 ErrorType: utils.IndirectDependencyFixNotSupported, 178 } 179 } 180 if len(depDetails.properties) > 0 { 181 return mph.updateProperties(&depDetails, vulnDetails.SuggestedFixedVersion) 182 } 183 184 return mph.updatePackageVersion(vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, depDetails.foundInDependencyManagement) 185 } 186 187 func (mph *MavenPackageHandler) getProjectPoms() (err error) { 188 // Check if we already scanned the project pom.xml locations 189 if len(mph.pomPaths) > 0 { 190 return 191 } 192 var depTreeOutput string 193 if depTreeOutput, err = mph.RunMavenDepTree(); err != nil { 194 err = fmt.Errorf("failed to get project poms while running maven-dep-tree: %s", err.Error()) 195 return 196 } 197 198 for _, jsonContent := range strings.Split(depTreeOutput, "\n") { 199 if jsonContent == "" { 200 continue 201 } 202 // Escape backslashes in the pomPath field, to fix windows backslash parsing issues 203 escapedContent := strings.ReplaceAll(jsonContent, `\`, `\\`) 204 var pp pomPath 205 if err = json.Unmarshal([]byte(escapedContent), &pp); err != nil { 206 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) 207 return 208 } 209 mph.pomPaths = append(mph.pomPaths, pp) 210 } 211 if len(mph.pomPaths) == 0 { 212 err = errors.New("couldn't find any pom.xml files in the current project") 213 } 214 return 215 } 216 217 // Update the package version. Updates it only if the version is not a reference to a property. 218 func (mph *MavenPackageHandler) updatePackageVersion(impactedPackage, fixedVersion string, foundInDependencyManagement bool) error { 219 updateVersionArgs := []string{ 220 "-U", "-B", "org.codehaus.mojo:versions-maven-plugin:use-dep-version", "-Dincludes=" + impactedPackage, 221 "-DdepVersion=" + fixedVersion, "-DgenerateBackupPoms=false", 222 fmt.Sprintf("-DprocessDependencies=%t", !foundInDependencyManagement), 223 fmt.Sprintf("-DprocessDependencyManagement=%t", foundInDependencyManagement)} 224 updateVersionCmd := fmt.Sprintf("mvn %s", strings.Join(updateVersionArgs, " ")) 225 log.Debug(fmt.Sprintf("Running '%s'", updateVersionCmd)) 226 output, err := mph.RunMvnCmd(updateVersionArgs) 227 if err != nil { 228 versionNotAvailableString := fmt.Sprintf(MavenVersionNotAvailableErrorFormat, fixedVersion) 229 // Replace Maven's 'version not available' error with more readable error message 230 if strings.Contains(string(output), versionNotAvailableString) { 231 err = fmt.Errorf("couldn't update %q to suggested fix version: %s", impactedPackage, versionNotAvailableString) 232 } 233 } 234 return err 235 } 236 237 // Update properties that represent this package's version. 238 func (mph *MavenPackageHandler) updateProperties(depDetails *pomDependencyDetails, fixedVersion string) error { 239 for _, property := range depDetails.properties { 240 updatePropertyArgs := []string{ 241 "-U", "-B", "org.codehaus.mojo:versions-maven-plugin:set-property", "-Dproperty=" + property, 242 "-DnewVersion=" + fixedVersion, "-DgenerateBackupPoms=false", 243 fmt.Sprintf("-DprocessDependencies=%t", !depDetails.foundInDependencyManagement), 244 fmt.Sprintf("-DprocessDependencyManagement=%t", depDetails.foundInDependencyManagement)} 245 updatePropertyCmd := fmt.Sprintf("mvn %s", strings.Join(updatePropertyArgs, " ")) 246 log.Debug(fmt.Sprintf("Running '%s'", updatePropertyCmd)) 247 if _, err := mph.RunMvnCmd(updatePropertyArgs); err != nil { // #nosec G204 248 return fmt.Errorf("failed updating %s property: %s\n", property, err.Error()) 249 } 250 } 251 return nil 252 }