1
1
package resolution
2
2
3
3
import (
4
+ "cmp"
4
5
"context"
5
6
"errors"
6
7
"fmt"
@@ -11,7 +12,6 @@ import (
11
12
"github.com/google/osv-scanner/internal/resolution/client"
12
13
"github.com/google/osv-scanner/internal/resolution/manifest"
13
14
"github.com/google/osv-scanner/pkg/models"
14
- "github.com/google/osv-scanner/pkg/osv"
15
15
)
16
16
17
17
type ResolutionVuln struct {
@@ -81,92 +81,131 @@ var OSVEcosystem = map[resolve.System]models.Ecosystem{
81
81
resolve .Maven : models .EcosystemMaven ,
82
82
}
83
83
84
- // computeVulns scans for vulnerabilities in a resolved graph and populates res.Vulns
85
- func (res * ResolutionResult ) computeVulns (ctx context.Context , cl resolve.Client ) error {
86
- // TODO: local vulnerability db support
87
- // TODO: when remediating, this is going to get called many times for the same packages, we should cache requests to the OSV API
88
- // Find all vulnerability IDs affecting each node in the graph.
89
- var request osv.BatchedQuery
90
- request .Queries = make ([]* osv.Query , len (res .Graph .Nodes )- 1 )
91
- for i , n := range res .Graph .Nodes [1 :] { // skipping the root node
92
- request .Queries [i ] = & osv.Query {
93
- Package : osv.Package {
94
- Name : n .Version .Name ,
95
- Ecosystem : string (OSVEcosystem [n .Version .System ]),
96
- },
97
- Version : n .Version .Version ,
84
+ // FilterVulns populates Vulns with the UnfilteredVulns that satisfy matchFn
85
+ func (res * ResolutionResult ) FilterVulns (matchFn func (ResolutionVuln ) bool ) {
86
+ var matchedVulns []ResolutionVuln
87
+ for _ , v := range res .UnfilteredVulns {
88
+ if matchFn (v ) {
89
+ matchedVulns = append (matchedVulns , v )
98
90
}
99
91
}
100
- response , err := osv .MakeRequest (request )
101
- if err != nil {
102
- return err
103
- }
104
- nodeVulns := response .Results
105
-
106
- // Get the details for each vulnerability
107
- // To save on request size, hydrate only unique IDs
108
- vulnInfo := make (map [string ]* models.Vulnerability )
109
- var hydrateQuery osv.BatchedResponse
110
- for _ , vulns := range nodeVulns {
111
- for _ , vuln := range vulns .Vulns {
112
- if _ , ok := vulnInfo [vuln .ID ]; ! ok {
113
- vulnInfo [vuln .ID ] = nil
114
- hydrateQuery .Results = append (hydrateQuery .Results , osv.MinimalResponse {Vulns : []osv.MinimalVulnerability {vuln }})
92
+ res .Vulns = matchedVulns
93
+ }
94
+
95
+ type ResolutionDiff struct {
96
+ Original * ResolutionResult
97
+ New * ResolutionResult
98
+ RemovedVulns []ResolutionVuln
99
+ AddedVulns []ResolutionVuln
100
+ manifest.ManifestPatch
101
+ }
102
+
103
+ func (res * ResolutionResult ) CalculateDiff (other * ResolutionResult ) ResolutionDiff {
104
+ diff := ResolutionDiff {
105
+ Original : res ,
106
+ New : other ,
107
+ ManifestPatch : manifest.ManifestPatch {Manifest : & res .Manifest },
108
+ }
109
+ // Find the changed requirements and the versions they resolve to
110
+ for i , oldReq := range res .Manifest .Requirements { // assuming these are in the same order and none are added/removed
111
+ newReq := other .Manifest .Requirements [i ]
112
+ if oldReq .Version == newReq .Version {
113
+ continue
114
+ }
115
+ // Find the node in the graph to find which actual version it resolved to
116
+ var oldResolved string
117
+ for _ , e := range res .Graph .Edges {
118
+ toNode := res .Graph .Nodes [e .To ]
119
+ if e .From == 0 && toNode .Version .PackageKey == oldReq .PackageKey {
120
+ oldResolved = toNode .Version .Version
121
+ break
115
122
}
116
123
}
117
- }
118
- //nolint:contextcheck // TODO: Should Hydrate be accepting a context?
119
- hydrated , err := osv .Hydrate (& hydrateQuery )
120
- if err != nil {
121
- return err
124
+ var newResolved string
125
+ for _ , e := range other .Graph .Edges {
126
+ toNode := other .Graph .Nodes [e .To ]
127
+ if e .From == 0 && toNode .Version .PackageKey == newReq .PackageKey {
128
+ newResolved = toNode .Version .Version
129
+ break
130
+ }
131
+ }
132
+ diff .Deps = append (diff .Deps , manifest.DependencyPatch {
133
+ Pkg : oldReq .PackageKey ,
134
+ Type : oldReq .Type .Clone (),
135
+ OrigRequire : oldReq .Version ,
136
+ OrigResolved : oldResolved ,
137
+ NewRequire : newReq .Version ,
138
+ NewResolved : newResolved ,
139
+ })
122
140
}
123
141
124
- for _ , resp := range hydrated .Results {
125
- for _ , vuln := range resp .Vulns {
126
- vuln := vuln
127
- vulnInfo [vuln .ID ] = & vuln
142
+ // Compute differences in present vulnerabilities.
143
+ // Currently this relies on vulnerability IDs being unique in the Vulns slice.
144
+ oldVulns := make (map [string ]int , len (res .Vulns ))
145
+ for i , v := range res .Vulns {
146
+ oldVulns [v .Vulnerability .ID ] = i
147
+ }
148
+ for _ , v := range other .Vulns {
149
+ if _ , ok := oldVulns [v .Vulnerability .ID ]; ok {
150
+ // The vuln already existed.
151
+ delete (oldVulns , v .Vulnerability .ID ) // delete so we know what's been removed
152
+ } else {
153
+ // This vuln was not in the original resolution - it was newly added
154
+ diff .AddedVulns = append (diff .AddedVulns , v )
128
155
}
129
156
}
157
+ // Any remaining oldVulns have been removed in the new resolution
158
+ for _ , idx := range oldVulns {
159
+ diff .RemovedVulns = append (diff .RemovedVulns , res .Vulns [idx ])
160
+ }
130
161
131
- // Find all dependency paths to the vulnerable dependencies
132
- var vulnerableNodes []resolve.NodeID
133
- var vulnNodeIdxs []int
134
- for i , vulns := range nodeVulns {
135
- if len (vulns .Vulns ) > 0 {
136
- vulnNodeIdxs = append (vulnNodeIdxs , i )
137
- vulnerableNodes = append (vulnerableNodes , resolve .NodeID (i + 1 ))
138
- }
162
+ return diff
163
+ }
164
+
165
+ // Compare compares ResolutionDiffs based on 'effectiveness' (best first):
166
+ //
167
+ // Sort order:
168
+ // 1. (number of fixed vulns - introduced vulns) / (number of changed direct dependencies) [descending]
169
+ // (i.e. more efficient first)
170
+ // 2. number of fixed vulns [descending]
171
+ // 3. number of changed direct dependencies [ascending]
172
+ // 4. changed direct dependency name package names [ascending]
173
+ // 5. size of changed direct dependency bump [ascending]
174
+ func (a ResolutionDiff ) Compare (b ResolutionDiff ) int {
175
+ // 1. (fixed - introduced) / (changes) [desc]
176
+ // Multiply out to avoid float casts
177
+ aRatio := (len (a .RemovedVulns ) - len (a .AddedVulns )) * (len (b .Deps ))
178
+ bRatio := (len (b .RemovedVulns ) - len (b .AddedVulns )) * (len (a .Deps ))
179
+ if c := cmp .Compare (aRatio , bRatio ); c != 0 {
180
+ return - c
139
181
}
140
- nodeChains := computeChains (res .Graph , vulnerableNodes )
141
- vulnChains := make (map [string ][]DependencyChain )
142
- for i , idx := range vulnNodeIdxs {
143
- for _ , vuln := range nodeVulns [idx ].Vulns {
144
- vulnChains [vuln .ID ] = append (vulnChains [vuln .ID ], nodeChains [i ]... )
145
- }
182
+
183
+ // 2. number of fixed vulns [desc]
184
+ if c := cmp .Compare (len (a .RemovedVulns ), len (b .RemovedVulns )); c != 0 {
185
+ return - c
146
186
}
147
187
148
- // construct the ResolutionVulns
149
- // TODO: This constructs a single ResolutionVuln per vulnerability ID.
150
- // The scan action treats vulns with the same ID but affecting different versions of a package as distinct.
151
- // TODO: Combine aliased IDs
152
- for id , vuln := range vulnInfo {
153
- rv := ResolutionVuln {Vulnerability : * vuln , DevOnly : true }
154
- for _ , chain := range vulnChains [id ] {
155
- if chainConstrains (ctx , cl , chain , vuln ) {
156
- rv .ProblemChains = append (rv .ProblemChains , chain )
157
- } else {
158
- rv .NonProblemChains = append (rv .NonProblemChains , chain )
159
- }
160
- rv .DevOnly = rv .DevOnly && ChainIsDev (chain , res .Manifest )
188
+ // 3. number of changed deps [asc]
189
+ if c := cmp .Compare (len (a .Deps ), len (b .Deps )); c != 0 {
190
+ return c
191
+ }
192
+
193
+ // 4. changed names [asc]
194
+ for i , aDep := range a .Deps {
195
+ bDep := b .Deps [i ]
196
+ if c := aDep .Pkg .Compare (bDep .Pkg ); c != 0 {
197
+ return c
161
198
}
162
- if len (rv .ProblemChains ) == 0 {
163
- // There has to be at least one problem chain for the vulnerability to appear.
164
- // If our heuristic couldn't determine any, treat them all as problematic.
165
- rv .ProblemChains = rv .NonProblemChains
166
- rv .NonProblemChains = nil
199
+ }
200
+
201
+ // 5. dependency bump amount [asc]
202
+ for i , aDep := range a .Deps {
203
+ bDep := b .Deps [i ]
204
+ sv := aDep .Pkg .Semver ()
205
+ if c := sv .Compare (aDep .NewResolved , bDep .NewResolved ); c != 0 {
206
+ return c
167
207
}
168
- res .Vulns = append (res .Vulns , rv )
169
208
}
170
209
171
- return nil
210
+ return 0
172
211
}
0 commit comments