github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/strategy/inplace/inplace.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 inplace implements the in-place remediation strategy. 16 package inplace 17 18 import ( 19 "cmp" 20 "context" 21 "slices" 22 23 "deps.dev/util/resolve" 24 "deps.dev/util/resolve/dep" 25 "deps.dev/util/semver" 26 "github.com/google/osv-scalibr/guidedremediation/internal/remediation" 27 "github.com/google/osv-scalibr/guidedremediation/internal/resolution" 28 "github.com/google/osv-scalibr/guidedremediation/internal/vulns" 29 "github.com/google/osv-scalibr/guidedremediation/options" 30 "github.com/google/osv-scalibr/guidedremediation/result" 31 "github.com/google/osv-scalibr/guidedremediation/upgrade" 32 "github.com/google/osv-scalibr/log" 33 osvpb "github.com/ossf/osv-schema/bindings/go/osvschema" 34 ) 35 36 // ComputePatches attempts to resolve each vulnerability found in the graph, 37 // returning the list of unique possible patches. 38 // Vulnerabilities are resolved by upgrading versions of vulnerable packages to compatible 39 // non-vulnerable versions. A version is compatible if it still satisfies the version constraints of 40 // all dependent packages, and its dependencies are satisfied by the existing graph. 41 func ComputePatches(ctx context.Context, cl resolve.Client, graph remediation.ResolvedGraph, opts *options.RemediationOptions) ([]result.Patch, error) { 42 if len(graph.Graph.Nodes) == 0 { 43 return nil, nil 44 } 45 sys := graph.Graph.Nodes[0].Version.Semver() 46 requiredVersions := computeAllVersionConstraints(graph.Vulns, sys) 47 type patch struct { 48 vk resolve.VersionKey 49 vulns []*osvpb.Vulnerability 50 } 51 vkPatches := make(map[resolve.VersionKey][]patch) 52 for _, v := range graph.Vulns { 53 for _, sg := range v.Subgraphs { 54 // Check if any of the existing patches fixes this vulnerability. 55 vk := sg.Nodes[sg.Dependency].Version 56 if opts.UpgradeConfig.Get(vk.Name) == upgrade.None { 57 // Don't try to fix vulns in packages that aren't allowed to be upgraded. 58 continue 59 } 60 foundFix := false 61 for i, p := range vkPatches[vk] { 62 if !vulns.IsAffected(v.OSV, vulns.VKToPackage(p.vk)) { 63 p.vulns = append(p.vulns, v.OSV) 64 foundFix = true 65 vkPatches[vk][i] = p 66 } 67 } 68 if foundFix { 69 continue 70 } 71 // No existing patch fixes this vulnerability, try find a new one. 72 found, ver := findLatestMatching(ctx, cl, graph, sg, v.OSV, requiredVersions, opts) 73 if !found { 74 continue 75 } 76 // Found a patch 77 newPatch := patch{ 78 vk: ver, 79 vulns: []*osvpb.Vulnerability{v.OSV}, 80 } 81 // Check the vulns of other patches if this patch also fixes them. 82 seenVulns := make(map[string]struct{}) 83 seenVulns[v.OSV.Id] = struct{}{} 84 for _, p := range vkPatches[vk] { 85 for _, vuln := range p.vulns { 86 if _, ok := seenVulns[vuln.Id]; !ok { 87 seenVulns[vuln.Id] = struct{}{} 88 if !vulns.IsAffected(vuln, vulns.VKToPackage(ver)) { 89 newPatch.vulns = append(newPatch.vulns, vuln) 90 } 91 } 92 } 93 } 94 vkPatches[vk] = append(vkPatches[vk], newPatch) 95 } 96 } 97 98 // Construct the result patches. 99 var resultPatches []result.Patch 100 for vk, patches := range vkPatches { 101 for _, p := range patches { 102 resultPatch := result.Patch{ 103 PackageUpdates: []result.PackageUpdate{ 104 result.PackageUpdate{ 105 Name: vk.Name, 106 VersionFrom: vk.Version, 107 VersionTo: p.vk.Version, 108 Transitive: true, 109 }, 110 }, 111 } 112 for _, vuln := range p.vulns { 113 resultPatch.Fixed = append(resultPatch.Fixed, result.Vuln{ 114 ID: vuln.Id, 115 Packages: []result.Package{ 116 result.Package{ 117 Name: vk.Name, 118 Version: vk.Version, 119 }, 120 }, 121 }) 122 } 123 slices.SortFunc(resultPatch.Fixed, func(a, b result.Vuln) int { return cmp.Compare(a.ID, b.ID) }) 124 resultPatch.Fixed = slices.CompactFunc(resultPatch.Fixed, func(a, b result.Vuln) bool { return a.ID == b.ID }) 125 resultPatches = append(resultPatches, resultPatch) 126 } 127 } 128 129 slices.SortFunc(resultPatches, func(a, b result.Patch) int { return a.Compare(b, sys) }) 130 return resultPatches, nil 131 } 132 133 // computeAllVersionConstraints computes the overall constraints on the versions of each vulnerable package. 134 func computeAllVersionConstraints(vulns []resolution.Vulnerability, sys semver.System) map[resolve.VersionKey]semver.Set { 135 requiredVersions := make(map[resolve.VersionKey]semver.Set) 136 for _, v := range vulns { 137 for _, sg := range v.Subgraphs { 138 node := sg.Nodes[sg.Dependency] 139 for _, p := range node.Parents { 140 set, err := parseContraint(sys, p.Requirement) 141 if err != nil { 142 log.Warnf("failed parsing constraint %s on package %s: %v", p.Requirement, node.Version.Name, err) 143 continue 144 } 145 if oldSet, ok := requiredVersions[node.Version]; ok { 146 if err := set.Intersect(oldSet); err != nil { 147 log.Warnf("failed intersecting constraints %s and %s on package %s: %v", p.Requirement, set.String(), node.Version.Name, err) 148 continue 149 } 150 } 151 requiredVersions[node.Version] = set 152 } 153 } 154 } 155 return requiredVersions 156 } 157 158 func parseContraint(sys semver.System, constraint string) (semver.Set, error) { 159 if sys == semver.NPM && constraint == "latest" { 160 // A 'latest' version is effectively meaningless in a lockfile, since what 'latest' is could have changed between locking. 161 constraint = "*" 162 } 163 c, err := sys.ParseConstraint(constraint) 164 if err != nil { 165 return semver.Set{}, err 166 } 167 return c.Set(), nil 168 } 169 170 func requirementsSatisfied(reqs []resolve.RequirementVersion, graph *resolve.Graph, depEdges []resolve.Edge, sys semver.System) bool { 171 for _, req := range reqs { 172 if req.Type.HasAttr(dep.Dev) { 173 // Dev-only dependencies are not installed. 174 continue 175 } 176 s, err := parseContraint(sys, req.Version) 177 if err != nil { 178 log.Warnf("failed parsing constraint %s on package %s: %v", req.Version, req.PackageKey, err) 179 return false 180 } 181 reqKnownAs, _ := req.Type.GetAttr(dep.KnownAs) 182 183 idx := slices.IndexFunc(depEdges, func(e resolve.Edge) bool { 184 if knownAs, _ := e.Type.GetAttr(dep.KnownAs); knownAs != reqKnownAs { 185 return false 186 } 187 node := graph.Nodes[e.To] 188 return node.Version.PackageKey == req.PackageKey 189 }) 190 if idx == -1 { 191 // No package of this version is in the graph - check if it's an optional dependency. 192 if req.Type.HasAttr(dep.Opt) || 193 // Sometimes optional dependencies can also be present in the regular dependencies section. 194 slices.ContainsFunc(reqs, func(r resolve.RequirementVersion) bool { 195 knownAs, _ := r.Type.GetAttr(dep.KnownAs) 196 return knownAs == reqKnownAs && r.PackageKey == req.PackageKey && r.Type.HasAttr(dep.Opt) 197 }) { 198 continue 199 } 200 return false 201 } 202 // Check if the package of this version matches the constraint. 203 node := graph.Nodes[depEdges[idx].To] 204 match, err := s.Match(node.Version.Version) 205 if err != nil { 206 log.Warnf("failed matching version %s to constraint %s on package %s: %v", node.Version.Version, s.String(), req.PackageKey, err) 207 return false 208 } 209 if !match { 210 return false 211 } 212 } 213 214 return true 215 } 216 217 func findLatestMatching(ctx context.Context, cl resolve.Client, graph remediation.ResolvedGraph, 218 sg *resolution.DependencySubgraph, v *osvpb.Vulnerability, 219 requiredVersions map[resolve.VersionKey]semver.Set, 220 opts *options.RemediationOptions) (bool, resolve.VersionKey) { 221 vk := sg.Nodes[sg.Dependency].Version 222 sys := vk.Semver() 223 vers, err := cl.Versions(ctx, vk.PackageKey) 224 if err != nil { 225 log.Errorf("failed to get versions for package %s: %v", vk.PackageKey, err) 226 return false, resolve.VersionKey{} 227 } 228 cmpFn := func(a, b resolve.Version) int { return sys.Compare(a.Version, b.Version) } 229 if !slices.IsSortedFunc(vers, cmpFn) { 230 vers = slices.Clone(vers) 231 slices.SortFunc(vers, cmpFn) 232 } 233 234 var depEdges []resolve.Edge 235 for _, e := range graph.Graph.Edges { 236 if e.From == sg.Dependency { 237 depEdges = append(depEdges, e) 238 } 239 } 240 241 // Find the latest version that still satisfies the constraints 242 for _, ver := range slices.Backward(vers) { 243 // Check that this is allowable to upgrade to this version. 244 _, diff, err := sys.Difference(vk.Version, ver.Version) 245 if err != nil { 246 log.Warnf("failed to compare versions %s and %s: %v", vk.Version, ver.Version, err) 247 continue 248 } 249 if !opts.UpgradeConfig.Get(vk.Name).Allows(diff) { 250 continue 251 } 252 253 // Check that this version is not vulnerable. 254 if vulns.IsAffected(v, vulns.VKToPackage(ver.VersionKey)) { 255 continue 256 } 257 258 // Check that this version satisfies the constraints of all dependent packages. 259 if s, ok := requiredVersions[vk]; ok { 260 match, err := s.Match(ver.Version) 261 if err != nil { 262 log.Warnf("failed matching version %s to constraints %s on package %s: %v", ver.Version, s.String(), vk.Name, err) 263 continue 264 } 265 if !match { 266 continue 267 } 268 } 269 270 // Check that all of this version's dependencies are satisfied by the existing graph. 271 reqs, err := cl.Requirements(ctx, ver.VersionKey) 272 if err != nil { 273 log.Warnf("failed to get requirements for package %s: %v", ver.VersionKey, err) 274 continue 275 } 276 if !requirementsSatisfied(reqs, graph.Graph, depEdges, sys) { 277 continue 278 } 279 280 // Found a patch 281 return true, ver.VersionKey 282 } 283 284 return false, resolve.VersionKey{} 285 }