github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/suggest/maven.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package suggest 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "slices" 22 "strings" 23 24 "deps.dev/util/resolve" 25 "deps.dev/util/semver" 26 "github.com/google/osv-scalibr/guidedremediation/internal/manifest" 27 mavenmanifest "github.com/google/osv-scalibr/guidedremediation/internal/manifest/maven" 28 "github.com/google/osv-scalibr/guidedremediation/options" 29 "github.com/google/osv-scalibr/guidedremediation/result" 30 "github.com/google/osv-scalibr/guidedremediation/upgrade" 31 "github.com/google/osv-scalibr/internal/mavenutil" 32 "github.com/google/osv-scalibr/log" 33 ) 34 35 // MavenSuggester suggests update patch for Maven dependencies. 36 type MavenSuggester struct{} 37 38 // Suggest returns the Patch to update Maven dependencies to a newer 39 // version based on the options. 40 func (ms *MavenSuggester) Suggest(ctx context.Context, mf manifest.Manifest, opts options.UpdateOptions) (result.Patch, error) { 41 specific, ok := mf.EcosystemSpecific().(mavenmanifest.ManifestSpecific) 42 if !ok { 43 return result.Patch{}, errors.New("invalid Maven ManifestSpecific data") 44 } 45 46 var packageUpdates []result.PackageUpdate 47 updated := make(map[resolve.VersionKey]bool) 48 for _, req := range append(mf.Requirements(), specific.RequirementsForUpdates...) { 49 if opts.UpgradeConfig.Get(req.Name) == upgrade.None { 50 continue 51 } 52 if opts.IgnoreDev && slices.Contains(mf.Groups()[mavenmanifest.MakeRequirementKey(req)], "test") { 53 // Skip the update if the dependency is of development group 54 // and updates on development dependencies are not desired 55 continue 56 } 57 if strings.Contains(req.Name, "${") || strings.Contains(req.Version, "${") { 58 // If there are unresolved properties, we should skip this version. 59 continue 60 } 61 if updated[req.VersionKey] { 62 // Skip the update if the dependency is already updated. 63 continue 64 } 65 updated[req.VersionKey] = true 66 67 latest, err := suggestMavenVersion(ctx, opts.ResolveClient, req, opts.UpgradeConfig.Get(req.Name)) 68 if err != nil { 69 log.Warnf("failed to suggest Maven version for package %s: %v", req.Name, err) 70 continue 71 } 72 if latest.Version == req.Version { 73 // No need to update 74 continue 75 } 76 77 pu := result.PackageUpdate{ 78 Name: req.Name, 79 VersionFrom: req.Version, 80 VersionTo: latest.Version, 81 Type: req.Type, 82 } 83 origDep := mavenmanifest.OriginalDependency(pu, specific.LocalRequirements) 84 if origDep.Name() != ":" { 85 // An empty name indicates the dependency is not found, so the original dependency is not in the base project. 86 // Only add a package update if it is from the base project. 87 packageUpdates = append(packageUpdates, pu) 88 } 89 } 90 91 return result.Patch{PackageUpdates: packageUpdates}, nil 92 } 93 94 // suggestMavenVersion returns the latest version based on the given Maven requirement version. 95 // If there is no newer version available, req will be returned. 96 // For a version range requirement, 97 // - the greatest version matching the constraint is assumed when deciding whether the 98 // update is a major update or not. 99 // - if the latest version does not satisfy the constraint, this version is returned; 100 // otherwise, the original version range requirement is returned. 101 func suggestMavenVersion(ctx context.Context, cl resolve.Client, req resolve.RequirementVersion, level upgrade.Level) (resolve.RequirementVersion, error) { 102 versions, err := cl.Versions(ctx, req.PackageKey) 103 if err != nil { 104 return resolve.RequirementVersion{}, fmt.Errorf("requesting versions of Maven package %s: %w", req.Name, err) 105 } 106 if len(versions) == 0 { 107 return resolve.RequirementVersion{}, fmt.Errorf("no versions found for Maven package %s", req.Name) 108 } 109 110 semvers := make([]*semver.Version, 0, len(versions)) 111 for _, ver := range versions { 112 parsed, err := semver.Maven.Parse(ver.Version) 113 if err != nil { 114 log.Warnf("parsing Maven version %s: %v", parsed, err) 115 continue 116 } 117 semvers = append(semvers, parsed) 118 } 119 120 constraint, err := semver.Maven.ParseConstraint(req.Version) 121 if err != nil { 122 return resolve.RequirementVersion{}, fmt.Errorf("parsing Maven constraint %s: %w", req.Version, err) 123 } 124 125 var current *semver.Version 126 if constraint.IsSimple() { 127 // Constraint is a simple version string, so can be parsed to a single version. 128 current, err = semver.Maven.Parse(req.Version) 129 if err != nil { 130 return resolve.RequirementVersion{}, fmt.Errorf("parsing Maven version %s: %w", req.Version, err) 131 } 132 } else { 133 // Guess the latest version satisfying the constraint is being used 134 for _, v := range semvers { 135 if constraint.MatchVersion(v) && current.Compare(v) < 0 { 136 current = v 137 } 138 } 139 } 140 141 var newReq *semver.Version 142 for _, v := range semvers { 143 if mavenutil.CompareVersions(req.VersionKey, v, newReq) < 0 { 144 // Skip versions smaller than the current requirement 145 continue 146 } 147 if _, diff := v.Difference(current); !level.Allows(diff) { 148 continue 149 } 150 if mavenutil.IsPrerelease(v, req.VersionKey) { 151 // Skip prerelease versions for updates considering that most people prefer stable, released 152 // versions for dependency updates. 153 continue 154 } 155 newReq = v 156 } 157 if constraint.IsSimple() || !constraint.MatchVersion(newReq) { 158 // For version range requirement, update the requirement if the 159 // new requirement does not satisfy the constraint. 160 req.Version = newReq.String() 161 } 162 163 return req, nil 164 }