github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/strategy/relax/relax.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 relax implements the relax remediation strategy. 16 package relax 17 18 import ( 19 "context" 20 "fmt" 21 "slices" 22 23 "deps.dev/util/resolve" 24 "github.com/google/osv-scalibr/enricher" 25 "github.com/google/osv-scalibr/guidedremediation/internal/remediation" 26 "github.com/google/osv-scalibr/guidedremediation/internal/resolution" 27 "github.com/google/osv-scalibr/guidedremediation/internal/strategy/common" 28 "github.com/google/osv-scalibr/guidedremediation/internal/strategy/relax/relaxer" 29 "github.com/google/osv-scalibr/guidedremediation/options" 30 "github.com/google/osv-scalibr/guidedremediation/upgrade" 31 ) 32 33 // ComputePatches attempts to resolve each vulnerability found in result independently, 34 // returning the list of unique possible patches. 35 // Vulnerabilities are resolved by relaxing version constraints of the direct dependencies that bring in the vulnerable packages. 36 // If a patch introduces new vulnerabilities, additional relaxations are attempted for the new vulnerabilities. 37 func ComputePatches(ctx context.Context, cl resolve.Client, ve enricher.Enricher, resolved *remediation.ResolvedManifest, opts *options.RemediationOptions) (common.PatchResult, error) { 38 patchFn := func(vulnIDs []string) common.StrategyResult { 39 patched, err := patchVulns(ctx, cl, ve, resolved, vulnIDs, opts) 40 return common.StrategyResult{ 41 VulnIDs: vulnIDs, 42 Resolved: patched, 43 Err: err} 44 } 45 46 return common.ComputePatches(patchFn, resolved, false) 47 } 48 49 // patchVulns tries to fix all vulns in vulnIDs by relaxing direct dependency versions. 50 // returns ErrPatchImpossible if all cannot be patched. 51 func patchVulns(ctx context.Context, cl resolve.Client, ve enricher.Enricher, resolved *remediation.ResolvedManifest, vulnIDs []string, opts *options.RemediationOptions) (*remediation.ResolvedManifest, error) { 52 resolved = &remediation.ResolvedManifest{ 53 Manifest: resolved.Manifest.Clone(), 54 ResolvedGraph: resolved.ResolvedGraph, 55 } 56 57 reqRelaxer, err := relaxer.ForEcosystem(resolved.Manifest.System()) 58 if err != nil { 59 return nil, err 60 } 61 toRelax := reqsToRelax(ctx, cl, resolved, vulnIDs, opts) 62 for len(toRelax) > 0 { 63 for _, req := range toRelax { 64 if opts.UpgradeConfig.Get(req.Name) == upgrade.None { 65 return nil, common.ErrPatchImpossible 66 } 67 newVer, ok := reqRelaxer.Relax(ctx, cl, req, opts.UpgradeConfig) 68 if !ok { 69 return nil, common.ErrPatchImpossible 70 } 71 if err := resolved.Manifest.PatchRequirement(newVer); err != nil { 72 return nil, fmt.Errorf("failed to patch requirement %v: %w", newVer, err) 73 } 74 } 75 76 // re-resolve the relaxed manifest 77 var err error 78 resolved.Graph, err = resolution.Resolve(ctx, cl, resolved.Manifest, opts.ResolutionOptions) 79 if err != nil { 80 return nil, err 81 } 82 resolved.UnfilteredVulns, err = resolution.FindVulnerabilities(ctx, ve, resolved.Manifest.Groups(), resolved.Graph) 83 if err != nil { 84 return nil, err 85 } 86 resolved.Vulns = slices.Clone(resolved.UnfilteredVulns) 87 resolved.Vulns = slices.DeleteFunc(resolved.Vulns, func(v resolution.Vulnerability) bool { return !remediation.MatchVuln(*opts, v) }) 88 toRelax = reqsToRelax(ctx, cl, resolved, vulnIDs, opts) 89 } 90 91 return resolved, nil 92 } 93 94 func reqsToRelax(ctx context.Context, cl resolve.Client, resolved *remediation.ResolvedManifest, vulnIDs []string, opts *options.RemediationOptions) []resolve.RequirementVersion { 95 var toRelax []resolve.RequirementVersion 96 for _, v := range resolved.Vulns { 97 if !slices.Contains(vulnIDs, v.OSV.Id) { 98 continue 99 } 100 // Only relax dependencies if their distance is less than MaxDepth 101 for _, sg := range v.Subgraphs { 102 constr := sg.ConstrainingSubgraph(ctx, cl, v.OSV) 103 for _, edge := range constr.Nodes[0].Children { 104 gNode := constr.Nodes[edge.To] 105 if opts.MaxDepth > 0 && gNode.Distance+1 > opts.MaxDepth { 106 continue 107 } 108 toRelax = append(toRelax, resolve.RequirementVersion{ 109 VersionKey: resolve.VersionKey{ 110 PackageKey: gNode.Version.PackageKey, 111 Version: edge.Requirement, 112 VersionType: resolve.Requirement, 113 }, 114 Type: edge.Type.Clone(), 115 }) 116 } 117 } 118 } 119 120 cmpFn := func(a, b resolve.RequirementVersion) int { 121 if cmp := a.Compare(b.VersionKey); cmp != 0 { 122 return cmp 123 } 124 return a.Type.Compare(b.Type) 125 } 126 slices.SortFunc(toRelax, cmpFn) 127 toRelax = slices.CompactFunc(toRelax, func(a, b resolve.RequirementVersion) bool { return cmpFn(a, b) == 0 }) 128 129 return toRelax 130 }