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  }