@@ -4,13 +4,15 @@ import (
4
4
"cmp"
5
5
"errors"
6
6
"fmt"
7
+ "log/slog"
7
8
"strconv"
8
9
"strings"
9
10
10
11
"slices"
11
12
12
13
"github.com/google/osv/vulnfeeds/cves"
13
14
"github.com/google/osv/vulnfeeds/git"
15
+ "github.com/google/osv/vulnfeeds/utility/logger"
14
16
"github.com/google/osv/vulnfeeds/vulns"
15
17
"github.com/ossf/osv-schema/bindings/go/osvschema"
16
18
)
@@ -72,7 +74,7 @@ func toVersionRangeType(s string) VersionRangeType {
72
74
// 3. If no versions are found, it falls back to searching for CPEs in the CNA container.
73
75
// 4. As a last resort, it attempts to extract version information from the description text (currently not saved).
74
76
// It returns the source of the version information and a slice of notes detailing the extraction process.
75
- func AddVersionInfo (cve cves.CVE5 , v * vulns.Vulnerability , metrics * ConversionMetrics ) {
77
+ func AddVersionInfo (cve cves.CVE5 , v * vulns.Vulnerability , metrics * ConversionMetrics , repos [] string ) {
76
78
gotVersions := false
77
79
78
80
// Combine 'affected' entries from both CNA and ADP containers.
@@ -104,31 +106,36 @@ func AddVersionInfo(cve cves.CVE5, v *vulns.Vulnerability, metrics *ConversionMe
104
106
hasGit = true
105
107
}
106
108
107
- aff := osvschema.Affected {}
108
- for _ , vr := range versionRanges {
109
- if versionType == VersionRangeTypeGit {
110
- vr .Type = osvschema .RangeGit
111
- vr .Repo = cveAff .Repo
112
- } else {
113
- vr .Type = osvschema .RangeEcosystem
114
- }
115
- aff .Ranges = append (aff .Ranges , vr )
116
- }
117
-
109
+ var aff osvschema.Affected
118
110
// Special handling for Linux kernel CVEs.
119
- if cve .Metadata .AssignerShortName == "Linux" && versionType != VersionRangeTypeGit {
120
- aff .Package = osvschema.Package {
121
- Ecosystem : string (osvschema .EcosystemLinux ),
122
- Name : "Kernel" ,
111
+ if cve .Metadata .AssignerShortName == "Linux" {
112
+ for _ , vr := range versionRanges {
113
+ if versionType == VersionRangeTypeGit {
114
+ vr .Type = osvschema .RangeGit
115
+ vr .Repo = cveAff .Repo
116
+ } else {
117
+ vr .Type = osvschema .RangeEcosystem
118
+ }
119
+ aff .Ranges = append (aff .Ranges , vr )
120
+ }
121
+ if versionType != VersionRangeTypeGit {
122
+ aff .Package = osvschema.Package {
123
+ Ecosystem : string (osvschema .EcosystemLinux ),
124
+ Name : "Kernel" ,
125
+ }
126
+ }
127
+ } else {
128
+ var err error
129
+ aff , err = gitVersionsToCommits (cve .Metadata .CVEID , versionRanges , repos , make (git.RepoTagsCache ))
130
+ if err != nil {
131
+ logger .Error ("Failed to convert git versions to commits" , slog .Any ("err" , err ))
132
+ } else {
133
+ hasGit = true
123
134
}
124
135
}
125
136
126
137
v .Affected = append (v .Affected , aff )
127
- if hasGit {
128
- metrics .AddSource (VersionSourceGit )
129
- } else {
130
- metrics .AddSource (VersionSourceAffected )
131
- }
138
+ metrics .VersionSources = append (metrics .VersionSources , VersionSourceAffected )
132
139
}
133
140
134
141
// If no versions were found so far, fall back to CPEs.
@@ -166,6 +173,103 @@ func AddVersionInfo(cve cves.CVE5, v *vulns.Vulnerability, metrics *ConversionMe
166
173
}
167
174
}
168
175
176
+ // resolveVersionToCommit is a helper to convert a version string to a commit hash.
177
+ // It logs the outcome of the conversion attempt and returns an empty string on failure.
178
+ func resolveVersionToCommit (cveID cves.CVEID , version , versionType , repo string , normalizedTags map [string ]git.NormalizedTag ) string {
179
+ if version == "" {
180
+ return ""
181
+ }
182
+ logger .Info ("Attempting to resolve version to commit" , slog .String ("cve" , string (cveID )), slog .String ("version" , version ), slog .String ("type" , versionType ), slog .String ("repo" , repo ))
183
+ commit , err := git .VersionToCommit (version , normalizedTags )
184
+ if err != nil {
185
+ logger .Warn ("Failed to get Git commit for version" , slog .String ("cve" , string (cveID )), slog .String ("version" , version ), slog .String ("type" , versionType ), slog .String ("repo" , repo ), slog .Any ("err" , err ))
186
+ return ""
187
+ }
188
+ logger .Info ("Successfully derived commit for version" , slog .String ("cve" , string (cveID )), slog .String ("commit" , commit ), slog .String ("version" , version ), slog .String ("type" , versionType ))
189
+
190
+ return commit
191
+ }
192
+
193
+ // Examines repos and tries to convert versions to commits by treating them as Git tags.
194
+ // Takes a CVE ID string (for logging), VersionInfo with AffectedVersions and
195
+ // typically no AffectedCommits and attempts to add AffectedCommits (including Fixed commits) where there aren't any.
196
+ // Refuses to add the same commit to AffectedCommits more than once.
197
+ func gitVersionsToCommits (cveID cves.CVEID , versionRanges []osvschema.Range , repos []string , cache git.RepoTagsCache ) (osvschema.Affected , error ) {
198
+ var newAff osvschema.Affected
199
+ var newVersionRanges []osvschema.Range
200
+ unresolvedRanges := versionRanges
201
+
202
+ for _ , repo := range repos {
203
+ if len (unresolvedRanges ) == 0 {
204
+ break // All ranges have been resolved.
205
+ }
206
+
207
+ normalizedTags , err := git .NormalizeRepoTags (repo , cache )
208
+ if err != nil {
209
+ logger .Warn ("Failed to normalize tags" , slog .String ("cve" , string (cveID )), slog .String ("repo" , repo ), slog .Any ("err" , err ))
210
+ continue
211
+ }
212
+
213
+ var stillUnresolvedRanges []osvschema.Range
214
+ for _ , vr := range unresolvedRanges {
215
+ var introduced , fixed , lastAffected string
216
+ for _ , e := range vr .Events {
217
+ if e .Introduced != "" {
218
+ introduced = e .Introduced
219
+ }
220
+ if e .Fixed != "" {
221
+ fixed = e .Fixed
222
+ }
223
+ if e .LastAffected != "" {
224
+ lastAffected = e .LastAffected
225
+ }
226
+ }
227
+
228
+ var introducedCommit string
229
+ if introduced == "0" {
230
+ introducedCommit = "0"
231
+ } else {
232
+ introducedCommit = resolveVersionToCommit (cveID , introduced , "introduced" , repo , normalizedTags )
233
+ }
234
+ fixedCommit := resolveVersionToCommit (cveID , fixed , "fixed" , repo , normalizedTags )
235
+ lastAffectedCommit := resolveVersionToCommit (cveID , lastAffected , "last_affected" , repo , normalizedTags )
236
+
237
+ if introducedCommit != "" && (fixedCommit != "" || lastAffectedCommit != "" ) {
238
+ var newVR osvschema.Range
239
+
240
+ if fixedCommit != "" {
241
+ newVR = buildVersionRange (introducedCommit , "" , fixedCommit )
242
+ } else {
243
+ newVR = buildVersionRange (introducedCommit , lastAffectedCommit , "" )
244
+ }
245
+
246
+ newVR .Repo = repo
247
+ newVR .Type = osvschema .RangeGit
248
+ newVR .DatabaseSpecific = make (map [string ]any )
249
+ newVR .DatabaseSpecific ["versions" ] = vr .Events
250
+ newVersionRanges = append (newVersionRanges , newVR )
251
+ } else {
252
+ stillUnresolvedRanges = append (stillUnresolvedRanges , vr )
253
+ }
254
+ }
255
+ unresolvedRanges = stillUnresolvedRanges
256
+ }
257
+
258
+ var err error
259
+ if len (unresolvedRanges ) > 0 {
260
+ newAff .DatabaseSpecific = make (map [string ]any )
261
+ newAff .DatabaseSpecific ["unresolved_versions" ] = unresolvedRanges
262
+ }
263
+
264
+ if len (newVersionRanges ) > 0 {
265
+ newAff .Ranges = newVersionRanges
266
+ } else if len (unresolvedRanges ) > 0 { // Only error if there were ranges to resolve but none were.
267
+ err = errors .New ("was not able to get git version ranges" )
268
+ }
269
+
270
+ return newAff , err
271
+ }
272
+
169
273
// findCPEVersionRanges extracts version ranges and CPE strings from the CNA's
170
274
// CPE applicability statements in a CVE record.
171
275
func findCPEVersionRanges (cve cves.CVE5 ) (versionRanges []osvschema.Range , cpes []string , err error ) {
@@ -345,7 +449,6 @@ func findNormalAffectedRanges(affected cves.Affected, metrics *ConversionMetrics
345
449
// affected, but more likely, it affects up to that version. It could also mean that the range is given
346
450
// in one line instead - like "< 1.5.3" or "< 2.45.4, >= 2.0 " or just "before 1.4.7", so check for that.
347
451
metrics .AddNote ("Only version exists" )
348
- // GitHub often encodes the range directly in the version string.
349
452
350
453
av , err := git .ParseVersionRange (vers .Version )
351
454
if err == nil {
0 commit comments