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  }