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  }