github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/strategy/relax/relaxer/npm.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 relaxer 16 17 import ( 18 "context" 19 "slices" 20 21 "deps.dev/util/resolve" 22 "deps.dev/util/semver" 23 "github.com/google/osv-scalibr/guidedremediation/upgrade" 24 "github.com/google/osv-scalibr/log" 25 ) 26 27 // NpmRelaxer implements RequirementRelaxer for npm. 28 type NpmRelaxer struct{} 29 30 // Relax attempts to relax import requirement. 31 // Returns the newly relaxed import and true it was successful. 32 // If unsuccessful, it returns the original import and false. 33 func (r NpmRelaxer) Relax(ctx context.Context, cl resolve.Client, req resolve.RequirementVersion, config upgrade.Config) (resolve.RequirementVersion, bool) { 34 configLevel := config.Get(req.Name) 35 if configLevel == upgrade.None { 36 return req, false 37 } 38 39 c, err := semver.NPM.ParseConstraint(req.Version) 40 if err != nil { 41 // The specified version is not a valid semver constraint 42 // Check if it's a version tag (usually 'latest') by seeing if there are matching versions 43 vks, err := cl.MatchingVersions(ctx, req.VersionKey) 44 if err != nil || len(vks) == 0 { // no matches, cannot relax 45 return req, false 46 } 47 // Use the first matching version (there should only be one) as a pinned version 48 c, err = semver.NPM.ParseConstraint(vks[0].Version) 49 if err != nil { 50 return req, false 51 } 52 } 53 54 // Get all the concrete versions of the package 55 allVKs, err := cl.Versions(ctx, req.PackageKey) 56 if err != nil { 57 return req, false 58 } 59 var vers []*semver.Version 60 for _, vk := range allVKs { 61 if vk.VersionType != resolve.Concrete { 62 continue 63 } 64 sv, err := semver.NPM.Parse(vk.Version) 65 if err != nil { 66 log.Warnf("failed to parse npm version %s: %v", vk.Version, err) 67 continue 68 } 69 vers = append(vers, sv) 70 } 71 slices.SortFunc(vers, func(a *semver.Version, b *semver.Version) int { 72 return a.Compare(b) 73 }) 74 75 // Find the versions on either side of the upper boundary of the requirement 76 var lastIdx int // highest version matching constraint 77 nextIdx := -1 // next version outside of range, preferring non-prerelease 78 nextIsPre := true // if the next version is a prerelease version 79 for lastIdx = len(vers) - 1; lastIdx >= 0; lastIdx-- { 80 v := vers[lastIdx] 81 if c.MatchVersion(v) { // found the upper bound, stop iterating 82 break 83 } 84 85 // Want to prefer non-prerelease versions, so only select one if we haven't seen any non-prerelease versions 86 if !v.IsPrerelease() || nextIsPre { 87 nextIdx = lastIdx 88 nextIsPre = v.IsPrerelease() 89 } 90 } 91 92 // Didn't find any higher versions of the package 93 if nextIdx == -1 { 94 return req, false 95 } 96 97 // No versions match the existing constraint, something is wrong 98 if lastIdx == -1 { 99 return req, false 100 } 101 102 // Our desired relaxation ordering is 103 // 1.2.3 -> 1.2.* -> 1.*.* -> 2.*.* -> 3.*.* -> ... 104 // But we want to use npm-like version specifiers e.g. 105 // 1.2.3 -> ~1.2.4 -> ^1.4.5 -> ^2.6.7 -> ^3.8.9 -> ... 106 // using the latest versions of the ranges 107 108 cmpVer := vers[lastIdx] 109 _, diff := cmpVer.Difference(vers[nextIdx]) 110 if !configLevel.Allows(diff) { 111 return req, false 112 } 113 if diff == semver.DiffMajor { 114 // Want to step only one major version at a time 115 // Instead of looking for a difference larger than major, 116 // we want to look for a major version bump from the first next version 117 cmpVer = vers[nextIdx] 118 diff = semver.DiffMinor 119 } 120 121 // Find the highest version with the same difference 122 best := vers[nextIdx] 123 for i := nextIdx + 1; i < len(vers); i++ { 124 _, d := cmpVer.Difference(vers[i]) 125 // If we've exceeded our allowed upgrade level, stop looking. 126 if !configLevel.Allows(d) { 127 break 128 } 129 130 // DiffMajor < DiffMinor < DiffPatch < DiffPrerelease 131 // So if d is less than the original diff, it represents a larger change 132 if d < diff { 133 break 134 } 135 if !vers[i].IsPrerelease() || nextIsPre { 136 best = vers[i] 137 } 138 } 139 140 if diff == semver.DiffPatch { 141 req.Version = "~" + best.String() 142 } else { 143 req.Version = "^" + best.String() 144 } 145 146 return req, true 147 }