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