github.com/google/osv-scalibr@v0.4.1/guidedremediation/result/result.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 result defines the remediation result structs 16 package result 17 18 import ( 19 "cmp" 20 21 "deps.dev/util/resolve/dep" 22 "deps.dev/util/semver" 23 "github.com/google/osv-scalibr/guidedremediation/strategy" 24 "github.com/ossf/osv-schema/bindings/go/osvconstants" 25 ) 26 27 // Result is a description of changes made by guided remediation to a manifest/lockfile. 28 type Result struct { 29 Path string `json:"path"` // path to the manifest/lockfile. 30 Ecosystem osvconstants.Ecosystem `json:"ecosystem"` // the OSV ecosystem of the file (npm, Maven) 31 Strategy strategy.Strategy `json:"strategy"` // the remediation strategy that was used. 32 Vulnerabilities []Vuln `json:"vulnerabilities"` // vulns detected in the initial manifest/lockfile. 33 Patches []Patch `json:"patches"` // list of dependency patches that were applied. 34 Errors []ResolveError `json:"errors,omitempty"` // non-fatal errors encountered in initial resolution. 35 } 36 37 // Vuln represents a vulnerability that was found in a project. 38 type Vuln struct { 39 ID string `json:"id"` // the OSV ID of the vulnerability. 40 Packages []Package `json:"packages"` // the list of packages in the dependency graph this vuln affects. 41 Unactionable bool `json:"unactionable,omitempty"` // true if no fix patch available, or if constraints would prevent one. 42 } 43 44 // Patch represents an isolated patch to one or more dependencies that fixes one or more vulns. 45 type Patch struct { 46 PackageUpdates []PackageUpdate `json:"packageUpdates"` // dependencies that were updated. 47 Fixed []Vuln `json:"fixed,omitempty"` // vulns fixed by this patch. 48 Introduced []Vuln `json:"introduced,omitempty"` // vulns introduced by this patch. 49 } 50 51 // Compare compares Patches based on 'effectiveness' (best first): 52 // 53 // Sort order: 54 // 1. (number of fixed vulns - introduced vulns) / (number of changed direct dependencies) [descending] 55 // (i.e. more efficient first) 56 // 2. number of fixed vulns [descending] 57 // 3. number of changed direct dependencies [ascending] 58 // 4. changed direct dependency name package names [ascending] 59 // 5. size of changed direct dependency bump [ascending] 60 func (a Patch) Compare(b Patch, sys semver.System) int { 61 // 1. (fixed - introduced) / (changes) [desc] 62 // Multiply out to avoid float casts 63 aRatio := (len(a.Fixed) - len(a.Introduced)) * (len(b.PackageUpdates)) 64 bRatio := (len(b.Fixed) - len(b.Introduced)) * (len(a.PackageUpdates)) 65 if c := cmp.Compare(aRatio, bRatio); c != 0 { 66 return -c 67 } 68 69 // 2. number of fixed vulns [desc] 70 if c := cmp.Compare(len(a.Fixed), len(b.Fixed)); c != 0 { 71 return -c 72 } 73 74 // 3. number of changed deps [asc] 75 if c := cmp.Compare(len(a.PackageUpdates), len(b.PackageUpdates)); c != 0 { 76 return c 77 } 78 79 // 4. changed names [asc] 80 for i, aDep := range a.PackageUpdates { 81 bDep := b.PackageUpdates[i] 82 if c := cmp.Compare(aDep.Name, bDep.Name); c != 0 { 83 return c 84 } 85 } 86 87 // 5. dependency bump amount [asc] 88 for i, aDep := range a.PackageUpdates { 89 bDep := b.PackageUpdates[i] 90 aVer, aErr := sys.Parse(aDep.VersionTo) 91 bVer, bErr := sys.Parse(bDep.VersionTo) 92 if aErr != nil || bErr != nil { 93 // Versions don't parse as single versions, most likely a range from relax. 94 // We can't easily compare the bounds of the range, so just do a string comparison. 95 if c := cmp.Compare(aDep.VersionTo, bDep.VersionTo); c != 0 { 96 return c 97 } 98 continue 99 } 100 101 if c := aVer.Compare(bVer); c != 0 { 102 return c 103 } 104 } 105 106 return 0 107 } 108 109 // Package represents a package that was found in a project. 110 type Package struct { 111 Name string `json:"name"` // name of the dependency. 112 Version string `json:"version"` // version of the dependency in the graph. 113 } 114 115 // PackageUpdate represents a package that was updated by a patch. 116 type PackageUpdate struct { 117 Name string `json:"name"` // name of dependency being updated. 118 VersionFrom string `json:"versionFrom"` // version of the dependency before the patch. 119 VersionTo string `json:"versionTo"` // version of the dependency after the patch. 120 Transitive bool `json:"transitive"` // false if this package is a direct dependency, true if indirect. 121 122 Type dep.Type `json:"-"` 123 } 124 125 // ResolveError represents an error encountered during the initial resolution of the dependency graph. 126 // 127 // e.g. 128 // 129 // ResolveError{ 130 // Package: OutputPackage{"foo", "1.2.3"}, 131 // Requirement: OutputPackage{"bar", ">2.0.0"}, 132 // Error: "could not find a version that satisfies requirement >2.0.0 for package bar", 133 // } 134 type ResolveError struct { 135 Package Package `json:"package"` // the package that caused the error. 136 Requirement Package `json:"requirement"` // the requirement of the package that errored. 137 Error string `json:"error"` // the error string. 138 }