github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/packagehandlers/gradlepackagehandler.go (about)

     1  package packagehandlers
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/jfrog/frogbot/utils"
     6  	"io/fs"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  )
    12  
    13  const (
    14  	groovyDescriptorFileSuffix    = "build.gradle"
    15  	kotlinDescriptorFileSuffix    = "build.gradle.kts"
    16  	apostrophes                   = "[\\\"|\\']"
    17  	directMapRegexpEntry          = "\\s*%s\\s*[:|=]\\s*"
    18  	directStringWithVersionFormat = "%s:%s:%s"
    19  )
    20  
    21  // Regexp pattern for "map" format dependencies
    22  // Example: group: "junit", name: "junit", version: "1.0.0" | group = "junit", name = "junit", version = "1.0.0"
    23  var directMapWithVersionRegexp = getMapRegexpEntry("group") + "," + getMapRegexpEntry("name") + "," + getMapRegexpEntry("version")
    24  
    25  func getMapRegexpEntry(mapEntry string) string {
    26  	return fmt.Sprintf(directMapRegexpEntry, mapEntry) + apostrophes + "%s" + apostrophes
    27  }
    28  
    29  type GradlePackageHandler struct {
    30  	CommonPackageHandler
    31  }
    32  
    33  func (gph *GradlePackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error {
    34  	if vulnDetails.IsDirectDependency {
    35  		return gph.updateDirectDependency(vulnDetails)
    36  	}
    37  
    38  	return &utils.ErrUnsupportedFix{
    39  		PackageName:  vulnDetails.ImpactedDependencyName,
    40  		FixedVersion: vulnDetails.SuggestedFixedVersion,
    41  		ErrorType:    utils.IndirectDependencyFixNotSupported,
    42  	}
    43  }
    44  
    45  func (gph *GradlePackageHandler) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) {
    46  	if !isVersionSupportedForFix(vulnDetails.ImpactedDependencyVersion) {
    47  		return &utils.ErrUnsupportedFix{
    48  			PackageName:  vulnDetails.ImpactedDependencyName,
    49  			FixedVersion: vulnDetails.SuggestedFixedVersion,
    50  			ErrorType:    utils.UnsupportedForFixVulnerableVersion,
    51  		}
    52  	}
    53  
    54  	// A gradle project may contain several descriptor files in several sub-modules. Each vulnerability may be found in each of the descriptor files.
    55  	// Therefore we iterate over every descriptor file for each vulnerability and try to find and fix it.
    56  	descriptorFilesPaths, err := getDescriptorFilesPaths()
    57  	if err != nil {
    58  		return
    59  	}
    60  
    61  	isAnyDescriptorFileChanged := false
    62  	for _, descriptorFilePath := range descriptorFilesPaths {
    63  		var isFileChanged bool
    64  		isFileChanged, err = gph.fixVulnerabilityIfExists(descriptorFilePath, vulnDetails)
    65  		if err != nil {
    66  			return
    67  		}
    68  		// We use logical OR to save information over all descriptor files whether there is at least one file that has been changed
    69  		isAnyDescriptorFileChanged = isAnyDescriptorFileChanged || isFileChanged
    70  	}
    71  
    72  	if !isAnyDescriptorFileChanged {
    73  		err = fmt.Errorf("impacted package '%s' was not found or could not be fixed in all descriptor files", vulnDetails.ImpactedDependencyName)
    74  	}
    75  	return
    76  }
    77  
    78  // Checks if the impacted version is currently supported for fix
    79  func isVersionSupportedForFix(impactedVersion string) bool {
    80  	if strings.Contains(impactedVersion, "+") ||
    81  		(strings.Contains(impactedVersion, "[") || strings.Contains(impactedVersion, "(")) ||
    82  		strings.Contains(impactedVersion, "latest.release") {
    83  		return false
    84  	}
    85  	return true
    86  }
    87  
    88  // Collects all descriptor files absolute paths
    89  func getDescriptorFilesPaths() (descriptorFilesPaths []string, err error) {
    90  	err = filepath.WalkDir(".", func(path string, d fs.DirEntry, innerErr error) error {
    91  		if innerErr != nil {
    92  			return fmt.Errorf("error has occured when trying to access or traverse the files system: %s", err.Error())
    93  		}
    94  
    95  		if strings.HasSuffix(path, groovyDescriptorFileSuffix) || strings.HasSuffix(path, kotlinDescriptorFileSuffix) {
    96  			var absFilePath string
    97  			absFilePath, innerErr = filepath.Abs(path)
    98  			if innerErr != nil {
    99  				return fmt.Errorf("couldn't retrieve file's absolute path for './%s':%s", path, innerErr.Error())
   100  			}
   101  			descriptorFilesPaths = append(descriptorFilesPaths, absFilePath)
   102  		}
   103  		return nil
   104  	})
   105  	return
   106  }
   107  
   108  // Fixes all direct occurrences of the given vulnerability in the given descriptor file, if vulnerability occurs
   109  func (gph *GradlePackageHandler) fixVulnerabilityIfExists(descriptorFilePath string, vulnDetails *utils.VulnerabilityDetails) (isFileChanged bool, err error) {
   110  	byteFileContent, err := os.ReadFile(descriptorFilePath)
   111  	if err != nil {
   112  		err = fmt.Errorf("couldn't read file '%s': %s", descriptorFilePath, err.Error())
   113  		return
   114  	}
   115  	fileContent := string(byteFileContent)
   116  	originalFile := fileContent
   117  
   118  	depGroup, depName, err := getVulnerabilityGroupAndName(vulnDetails.ImpactedDependencyName)
   119  	if err != nil {
   120  		return
   121  	}
   122  
   123  	// Fixing all vulnerable rows given in a string format. For Example: implementation "junit:junit:4.7"
   124  	directStringVulnerableRow := fmt.Sprintf(directStringWithVersionFormat, depGroup, depName, vulnDetails.ImpactedDependencyVersion)
   125  	directStringFixedRow := fmt.Sprintf(directStringWithVersionFormat, depGroup, depName, vulnDetails.SuggestedFixedVersion)
   126  	fileContent = strings.ReplaceAll(fileContent, directStringVulnerableRow, directStringFixedRow)
   127  
   128  	// We replace '.' characters to '\\.' since '.' in order to correctly capture '.' character using regexps
   129  	regexpAdjustedDepGroup := strings.ReplaceAll(depGroup, ".", "\\.")
   130  	regexpAdjustedDepName := strings.ReplaceAll(depName, ".", "\\.")
   131  	regexpAdjustedImpactedVersion := strings.ReplaceAll(vulnDetails.ImpactedDependencyVersion, ".", "\\.")
   132  
   133  	// Fixing all vulnerable rows given in a map format. For Example: implementation group: "junit", name: "junit", version: "4.7"
   134  	mapRegexpForVulnerability := fmt.Sprintf(directMapWithVersionRegexp, regexpAdjustedDepGroup, regexpAdjustedDepName, regexpAdjustedImpactedVersion)
   135  	regexpCompiler := regexp.MustCompile(mapRegexpForVulnerability)
   136  	if rowsMatches := regexpCompiler.FindAllString(fileContent, -1); rowsMatches != nil {
   137  		for _, entry := range rowsMatches {
   138  			fixedRow := strings.Replace(entry, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion, 1)
   139  			fileContent = strings.ReplaceAll(fileContent, entry, fixedRow)
   140  		}
   141  	}
   142  
   143  	// If there is no changes in the file we finish dealing with the current descriptor file
   144  	if fileContent == originalFile {
   145  		return
   146  	}
   147  	isFileChanged = true
   148  
   149  	err = writeUpdatedBuildFile(descriptorFilePath, fileContent)
   150  	return
   151  }
   152  
   153  // 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
   154  func getVulnerabilityGroupAndName(impactedDependencyName string) (depGroup string, depName string, err error) {
   155  	seperatedImpactedDepName := strings.Split(impactedDependencyName, ":")
   156  	if len(seperatedImpactedDepName) != 2 {
   157  		err = fmt.Errorf("unable to parse impacted dependency name '%s'", impactedDependencyName)
   158  		return
   159  	}
   160  	return seperatedImpactedDepName[0], seperatedImpactedDepName[1], err
   161  }
   162  
   163  // Writes the updated content of the descriptor's file into the file
   164  func writeUpdatedBuildFile(filePath string, fileContent string) (err error) {
   165  	fileInfo, err := os.Stat(filePath)
   166  	if err != nil {
   167  		err = fmt.Errorf("couldn't get file info for file '%s': %s", filePath, err.Error())
   168  		return
   169  	}
   170  
   171  	err = os.WriteFile(filePath, []byte(fileContent), fileInfo.Mode())
   172  	if err != nil {
   173  		err = fmt.Errorf("couldn't write fixes to file '%s': %q", filePath, err)
   174  	}
   175  	return
   176  }