-
Notifications
You must be signed in to change notification settings - Fork 79
/
Copy pathmavenpackagehandler.go
279 lines (252 loc) · 10.3 KB
/
mavenpackagehandler.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
package packagehandlers
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"github.com/jfrog/frogbot/v2/utils"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/java"
"github.com/jfrog/jfrog-client-go/utils/log"
"golang.org/x/exp/slices"
"os"
"path/filepath"
"strings"
)
const MavenVersionNotAvailableErrorFormat = "Version %s is not available for artifact"
type gavCoordinate struct {
GroupId string `xml:"groupId"`
ArtifactId string `xml:"artifactId"`
Version string `xml:"version"`
foundInDependencyManagement bool
}
func (gc *gavCoordinate) isEmpty() bool {
return gc.GroupId == "" && gc.ArtifactId == "" && gc.Version == ""
}
func (gc *gavCoordinate) trimSpaces() *gavCoordinate {
gc.GroupId = strings.TrimSpace(gc.GroupId)
gc.ArtifactId = strings.TrimSpace(gc.ArtifactId)
gc.Version = strings.TrimSpace(gc.Version)
return gc
}
type mavenDependency struct {
gavCoordinate
Dependencies []mavenDependency `xml:"dependencies>dependency"`
DependencyManagement []mavenDependency `xml:"dependencyManagement>dependencies>dependency"`
Plugins []mavenPlugin `xml:"build>plugins>plugin"`
}
func (md *mavenDependency) collectMavenDependencies(foundInDependencyManagement bool) []gavCoordinate {
var result []gavCoordinate
if !md.isEmpty() {
md.foundInDependencyManagement = foundInDependencyManagement
result = append(result, *md.trimSpaces())
}
for _, dependency := range md.Dependencies {
result = append(result, dependency.collectMavenDependencies(foundInDependencyManagement)...)
}
for _, dependency := range md.DependencyManagement {
result = append(result, dependency.collectMavenDependencies(true)...)
}
for _, plugin := range md.Plugins {
result = append(result, plugin.collectMavenPlugins()...)
}
return result
}
type mavenPlugin struct {
gavCoordinate
NestedPlugins []mavenPlugin `xml:"configuration>plugins>plugin"`
}
func (mp *mavenPlugin) collectMavenPlugins() []gavCoordinate {
var result []gavCoordinate
if !mp.isEmpty() {
result = append(result, *mp.trimSpaces())
}
for _, plugin := range mp.NestedPlugins {
result = append(result, plugin.collectMavenPlugins()...)
}
return result
}
// fillDependenciesMap collects direct dependencies from the pomPath pom.xml file.
// If the version of a dependency is set in another property section, it is added as its value in the map.
func (mph *MavenPackageHandler) fillDependenciesMap(pomPath string) error {
contentBytes, err := os.ReadFile(filepath.Clean(pomPath))
if err != nil {
return errors.New("couldn't read pom.xml file: " + err.Error())
}
mavenDependencies, err := getMavenDependencies(contentBytes)
if err != nil {
return err
}
for _, dependency := range mavenDependencies {
if dependency.Version == "" {
continue
}
depName := fmt.Sprintf("%s:%s", dependency.GroupId, dependency.ArtifactId)
if _, exist := mph.pomDependencies[depName]; !exist {
mph.pomDependencies[depName] = pomDependencyDetails{foundInDependencyManagement: dependency.foundInDependencyManagement, currentVersion: dependency.Version}
}
if strings.HasPrefix(dependency.Version, "${") {
trimmedVersion := strings.Trim(dependency.Version, "${}")
if !slices.Contains(mph.pomDependencies[depName].properties, trimmedVersion) {
mph.pomDependencies[depName] = pomDependencyDetails{
properties: append(mph.pomDependencies[depName].properties, trimmedVersion),
currentVersion: dependency.Version,
foundInDependencyManagement: dependency.foundInDependencyManagement,
}
}
}
}
return nil
}
// Extract all dependencies from the input pom.xml
// pomXmlContent - The pom.xml content
func getMavenDependencies(pomXmlContent []byte) (result []gavCoordinate, err error) {
var dependencies mavenDependency
if err = xml.Unmarshal(pomXmlContent, &dependencies); err != nil {
err = fmt.Errorf("failed to unmarshal the current pom.xml:\n%s, error received:\n%w"+string(pomXmlContent), err)
return
}
result = append(result, dependencies.collectMavenDependencies(false)...)
return
}
type pomPath struct {
PomPath string `json:"pomPath"`
}
type pomDependencyDetails struct {
properties []string
currentVersion string
foundInDependencyManagement bool
}
func NewMavenPackageHandler(scanDetails *utils.ScanDetails) *MavenPackageHandler {
depTreeParams := &java.DepTreeParams{
Server: scanDetails.ServerDetails,
DepsRepo: scanDetails.DepsRepo,
IsMavenDepTreeInstalled: true,
}
// The mvn-dep-tree plugin has already been installed during the audit dependency tree build phase,
// Therefore, we set the `isDepTreeInstalled` flag to true
mavenDepTreeManager := java.NewMavenDepTreeManager(depTreeParams, java.Projects)
return &MavenPackageHandler{MavenDepTreeManager: mavenDepTreeManager}
}
type MavenPackageHandler struct {
CommonPackageHandler
// pomDependencies holds a map of direct dependencies found in pom.xml.
pomDependencies map[string]pomDependencyDetails
// pomPaths holds the paths to all the pom.xml files that are related to the current project.
pomPaths []pomPath
// mavenDepTreeManager handles the installation and execution of the maven-dep-tree to obtain all the project poms and running mvn commands
*java.MavenDepTreeManager
}
func (mph *MavenPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) (err error) {
// When resolution from an Artifactory server is necessary, a settings.xml file will be generated, and its path will be set in mph.
if mph.GetDepsRepo() != "" {
var clearMavenDepTreeRun func() error
_, clearMavenDepTreeRun, err = mph.CreateTempDirWithSettingsXmlIfNeeded()
if err != nil {
return
}
defer func() {
err = errors.Join(err, clearMavenDepTreeRun())
}()
}
err = mph.getProjectPoms()
if err != nil {
return err
}
// Get direct dependencies for each pom.xml file
if mph.pomDependencies == nil {
mph.pomDependencies = make(map[string]pomDependencyDetails)
}
for _, pp := range mph.pomPaths {
if err = mph.fillDependenciesMap(pp.PomPath); err != nil {
return err
}
}
var depDetails pomDependencyDetails
var exists bool
// Check if the impacted package is a direct dependency
impactedDependency := vulnDetails.ImpactedDependencyName
if depDetails, exists = mph.pomDependencies[impactedDependency]; !exists {
return &utils.ErrUnsupportedFix{
PackageName: vulnDetails.ImpactedDependencyName,
FixedVersion: vulnDetails.SuggestedFixedVersion,
ErrorType: utils.IndirectDependencyFixNotSupported,
}
}
if len(depDetails.properties) > 0 {
return mph.updateProperties(&depDetails, vulnDetails.SuggestedFixedVersion)
}
return mph.updatePackageVersion(vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, depDetails.foundInDependencyManagement)
}
// Returns project's Pom paths. This function requires an execution of maven-dep-tree 'project' command prior to its execution
func (mph *MavenPackageHandler) getProjectPoms() (err error) {
// Check if we already scanned the project pom.xml locations
if len(mph.pomPaths) > 0 {
return
}
oldSettingsXmlPath := mph.GetSettingsXmlPath()
var depTreeOutput string
var clearMavenDepTreeRun func() error
if depTreeOutput, clearMavenDepTreeRun, err = mph.RunMavenDepTree(); err != nil {
err = fmt.Errorf("failed to get project poms while running maven-dep-tree: %s", err.Error())
if clearMavenDepTreeRun != nil {
err = errors.Join(err, clearMavenDepTreeRun())
}
return
}
defer func() {
err = clearMavenDepTreeRun()
mph.SetSettingsXmlPath(oldSettingsXmlPath)
}()
for _, jsonContent := range strings.Split(depTreeOutput, "\n") {
if jsonContent == "" {
continue
}
// Escape backslashes in the pomPath field, to fix windows backslash parsing issues
escapedContent := strings.ReplaceAll(jsonContent, `\`, `\\`)
var pp pomPath
if err = json.Unmarshal([]byte(escapedContent), &pp); err != nil {
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)
return
}
mph.pomPaths = append(mph.pomPaths, pp)
}
if len(mph.pomPaths) == 0 {
err = errors.New("couldn't find any pom.xml files in the current project")
}
return
}
// Update the package version. Updates it only if the version is not a reference to a property.
func (mph *MavenPackageHandler) updatePackageVersion(impactedPackage, fixedVersion string, foundInDependencyManagement bool) error {
updateVersionArgs := []string{
"-U", "-B", "org.codehaus.mojo:versions-maven-plugin:use-dep-version", "-Dincludes=" + impactedPackage,
"-DdepVersion=" + fixedVersion, "-DgenerateBackupPoms=false",
fmt.Sprintf("-DprocessDependencies=%t", !foundInDependencyManagement),
fmt.Sprintf("-DprocessDependencyManagement=%t", foundInDependencyManagement)}
updateVersionCmd := fmt.Sprintf("mvn %s", strings.Join(updateVersionArgs, " "))
log.Debug(fmt.Sprintf("Running '%s'", updateVersionCmd))
output, err := mph.RunMvnCmd(updateVersionArgs)
if err != nil {
versionNotAvailableString := fmt.Sprintf(MavenVersionNotAvailableErrorFormat, fixedVersion)
// Replace Maven's 'version not available' error with more readable error message
if strings.Contains(string(output), versionNotAvailableString) {
err = fmt.Errorf("couldn't update %q to suggested fix version: %s", impactedPackage, versionNotAvailableString)
}
}
return err
}
// Update properties that represent this package's version.
func (mph *MavenPackageHandler) updateProperties(depDetails *pomDependencyDetails, fixedVersion string) error {
for _, property := range depDetails.properties {
updatePropertyArgs := []string{
"-U", "-B", "org.codehaus.mojo:versions-maven-plugin:set-property", "-Dproperty=" + property,
"-DnewVersion=" + fixedVersion, "-DgenerateBackupPoms=false",
fmt.Sprintf("-DprocessDependencies=%t", !depDetails.foundInDependencyManagement),
fmt.Sprintf("-DprocessDependencyManagement=%t", depDetails.foundInDependencyManagement)}
updatePropertyCmd := fmt.Sprintf("mvn %s", strings.Join(updatePropertyArgs, " "))
log.Debug(fmt.Sprintf("Running '%s'", updatePropertyCmd))
if _, err := mph.RunMvnCmd(updatePropertyArgs); err != nil { // #nosec G204
return fmt.Errorf("failed updating %s property: %s\n", property, err.Error())
}
}
return nil
}