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 }