Skip to content

Commit 3b73304

Browse files
authored
refactor(vulnfeeds): pass metrics struct instead of notes array (#4054)
1 parent f5fbf57 commit 3b73304

File tree

5 files changed

+49
-53
lines changed

5 files changed

+49
-53
lines changed

vulnfeeds/cmd/cvelist2osv/converter.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ func extractConversionMetrics(cve cves.CVE5, refs []osvschema.Reference, metrics
6464
// FromCVE5 creates a `vulns.Vulnerability` object from a `cves.CVE5` object.
6565
// It populates the main fields of the OSV record, including ID, summary, details,
6666
// references, timestamps, severity, and version information.
67-
func FromCVE5(cve cves.CVE5, refs []cves.Reference, metrics *ConversionMetrics) (*vulns.Vulnerability, []string) {
67+
func FromCVE5(cve cves.CVE5, refs []cves.Reference, metrics *ConversionMetrics) *vulns.Vulnerability {
6868
aliases, related := vulns.ExtractReferencedVulns(cve.Metadata.CVEID, cve.Metadata.CVEID, refs)
69-
var notes []string
7069
v := vulns.Vulnerability{
7170
Vulnerability: osvschema.Vulnerability{
7271
SchemaVersion: osvschema.SchemaVersion,
@@ -80,22 +79,21 @@ func FromCVE5(cve cves.CVE5, refs []cves.Reference, metrics *ConversionMetrics)
8079

8180
published, err := cves.ParseCVE5Timestamp(cve.Metadata.DatePublished)
8281
if err != nil {
83-
notes = append(notes, "Published date failed to parse, setting time to now")
82+
metrics.Notes = append(metrics.Notes, "Published date failed to parse, setting time to now")
8483
published = time.Now()
8584
}
8685
v.Published = published
8786

8887
modified, err := cves.ParseCVE5Timestamp(cve.Metadata.DateUpdated)
8988
if err != nil {
90-
notes = append(notes, "Modified date failed to parse, setting time to now")
89+
metrics.Notes = append(metrics.Notes, "Modified date failed to parse, setting time to now")
9190
modified = time.Now()
9291
}
9392
v.Modified = modified
9493

9594
// Add affected version information.
96-
versionSources, versNotes := AddVersionInfo(cve, &v)
97-
notes = append(notes, versNotes...)
98-
metrics.VersionSources = versionSources
95+
AddVersionInfo(cve, &v, metrics)
96+
9997
// TODO(jesslowe@): Add CWEs.
10098

10199
// Combine severity metrics from both CNA and ADP containers.
@@ -114,7 +112,7 @@ func FromCVE5(cve cves.CVE5, refs []cves.Reference, metrics *ConversionMetrics)
114112
}
115113
}
116114

117-
return &v, notes
115+
return &v
118116
}
119117

120118
// writeOSVToFile saves the generated OSV vulnerability record to a JSON file.
@@ -170,8 +168,7 @@ func ConvertAndExportCVEToOSV(cve cves.CVE5, directory string) error {
170168
references := identifyPossibleURLs(cve)
171169
metrics := &ConversionMetrics{}
172170
// Create a base OSV record from the CVE.
173-
v, notes := FromCVE5(cve, references, metrics)
174-
metrics.Notes = append(metrics.Notes, notes...)
171+
v := FromCVE5(cve, references, metrics)
175172

176173
// Collect metrics about the conversion.
177174
extractConversionMetrics(cve, v.References, metrics)

vulnfeeds/cmd/cvelist2osv/converter_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ func TestFromCVE5(t *testing.T) {
353353
for _, tc := range testCases {
354354
t.Run(tc.name, func(t *testing.T) {
355355
metrics := &ConversionMetrics{}
356-
vuln, _ := FromCVE5(tc.cve, tc.refs, metrics)
356+
vuln := FromCVE5(tc.cve, tc.refs, metrics)
357357

358358
// Handle non-deterministic time.Now()
359359
if strings.Contains(tc.name, "invalid date") {

vulnfeeds/cmd/cvelist2osv/version_extraction.go

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ func toVersionRangeType(s string) VersionRangeType {
7272
// 3. If no versions are found, it falls back to searching for CPEs in the CNA container.
7373
// 4. As a last resort, it attempts to extract version information from the description text (currently not saved).
7474
// 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) ([]VersionSource, []string) {
76-
var notes []string
77-
var source []VersionSource
75+
func AddVersionInfo(cve cves.CVE5, v *vulns.Vulnerability, metrics *ConversionMetrics) {
7876
gotVersions := false
7977

8078
// Combine 'affected' entries from both CNA and ADP containers.
@@ -91,12 +89,11 @@ func AddVersionInfo(cve cves.CVE5, v *vulns.Vulnerability) ([]VersionSource, []s
9189
// Attempt to extract version ranges from the combined 'affected' fields.
9290
hasGit := false
9391
for _, cveAff := range affected {
94-
versionRanges, versionType, extractNotes := extractVersionsFromAffectedField(cveAff, cve.Metadata.AssignerShortName)
92+
versionRanges, versionType := extractVersionsFromAffectedField(cveAff, cve.Metadata.AssignerShortName, metrics)
9593
// TODO(jesslowe): update this to be more elegant (currently skips retrieving more git ranges after the first)
9694
if versionType == VersionRangeTypeGit && hasGit {
9795
continue
9896
}
99-
notes = append(notes, extractNotes...)
10097

10198
if len(versionRanges) == 0 {
10299
continue
@@ -128,19 +125,19 @@ func AddVersionInfo(cve cves.CVE5, v *vulns.Vulnerability) ([]VersionSource, []s
128125

129126
v.Affected = append(v.Affected, aff)
130127
if hasGit {
131-
source = append(source, VersionSourceGit)
128+
metrics.VersionSources = append(metrics.VersionSources, VersionSourceGit)
132129
} else {
133-
source = append(source, VersionSourceAffected)
130+
metrics.VersionSources = append(metrics.VersionSources, VersionSourceAffected)
134131
}
135132
}
136133

137134
// If no versions were found so far, fall back to CPEs.
138135
if !gotVersions {
139-
notes = append(notes, "No versions in affected, attempting to extract from CPE")
136+
metrics.Notes = append(metrics.Notes, "No versions in affected, attempting to extract from CPE")
140137
cpeRanges, cpeStrings, err := findCPEVersionRanges(cve)
141138
if err == nil && len(cpeRanges) > 0 {
142139
gotVersions = true
143-
source = append(source, VersionSourceCPE)
140+
metrics.VersionSources = append(metrics.VersionSources, VersionSourceCPE)
144141
aff := osvschema.Affected{}
145142
for _, vr := range cpeRanges {
146143
vr.Type = osvschema.RangeEcosystem
@@ -150,23 +147,21 @@ func AddVersionInfo(cve cves.CVE5, v *vulns.Vulnerability) ([]VersionSource, []s
150147
aff.DatabaseSpecific["CPEs"] = vulns.Unique(cpeStrings)
151148
v.Affected = append(v.Affected, aff)
152149
} else if err != nil {
153-
notes = append(notes, err.Error())
150+
metrics.Notes = append(metrics.Notes, err.Error())
154151
}
155152
}
156153

157154
// As a last resort, try extracting versions from the description text.
158155
if !gotVersions {
159-
notes = append(notes, "No versions in CPEs so attempting extraction from description")
156+
metrics.Notes = append(metrics.Notes, "No versions in CPEs so attempting extraction from description")
160157
versions, extractNotes := cves.ExtractVersionsFromText(nil, cves.EnglishDescription(cve.Containers.CNA.Descriptions))
161-
notes = append(notes, extractNotes...)
158+
metrics.Notes = append(metrics.Notes, extractNotes...)
162159
if len(versions) > 0 {
163160
// NOTE: These versions are not currently saved due to the need for better validation.
164-
source = append(source, VersionSourceDescription)
165-
notes = append(notes, fmt.Sprintf("Extracted versions from description but did not save them: %+v", versions))
161+
metrics.VersionSources = append(metrics.VersionSources, VersionSourceDescription)
162+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("Extracted versions from description but did not save them: %+v", versions))
166163
}
167164
}
168-
169-
return source, notes
170165
}
171166

172167
// findCPEVersionRanges extracts version ranges and CPE strings from the CNA's
@@ -214,23 +209,24 @@ func findCPEVersionRanges(cve cves.CVE5) (versionRanges []osvschema.Range, cpes
214209
// - As a fallback, it may assume a single version means "fixed at this version, introduced at 0".
215210
//
216211
// Returns the extracted OSV ranges, the most frequent version type (e.g., "semver"), and any notes.
217-
func extractVersionsFromAffectedField(affected cves.Affected, cnaAssigner string) ([]osvschema.Range, VersionRangeType, []string) {
212+
func extractVersionsFromAffectedField(affected cves.Affected, cnaAssigner string, metrics *ConversionMetrics) ([]osvschema.Range, VersionRangeType) {
218213
// Handle cases where a product is marked as "affected" by default, and specific versions are marked "unaffected".
219214
if affected.DefaultStatus == "affected" {
220215
// Calculate the affected ranges by finding the inverse of the unaffected ranges.
221-
return findInverseAffectedRanges(affected, cnaAssigner)
216+
return findInverseAffectedRanges(affected, cnaAssigner, metrics)
222217
}
223218

224-
return findNormalAffectedRanges(affected, cnaAssigner)
219+
return findNormalAffectedRanges(affected, cnaAssigner, metrics)
225220
}
226221

227222
// findInverseAffectedRanges calculates the affected version ranges by analyzing a list
228223
// of 'unaffected' versions. This is common in Linux kernel CVEs where a product is
229224
// considered affected by default, and only unaffected versions are listed.
230225
// It sorts the introduced and fixed versions to create chronological ranges.
231-
func findInverseAffectedRanges(cveAff cves.Affected, cnaAssigner string) (ranges []osvschema.Range, versType VersionRangeType, notes []string) {
226+
func findInverseAffectedRanges(cveAff cves.Affected, cnaAssigner string, metrics *ConversionMetrics) (ranges []osvschema.Range, versType VersionRangeType) {
232227
if cnaAssigner != "Linux" {
233-
return nil, VersionRangeTypeUnknown, append(notes, "Currently only supporting Linux inverse logic")
228+
metrics.Notes = append(metrics.Notes, "Currently only supporting Linux inverse logic")
229+
return nil, VersionRangeTypeUnknown
234230
}
235231
var introduced []string
236232
fixed := make([]string, 0, len(cveAff.Versions))
@@ -244,7 +240,7 @@ func findInverseAffectedRanges(cveAff cves.Affected, cnaAssigner string) (ranges
244240
case 3:
245241
introduced = append(introduced, versionValue)
246242
default:
247-
notes = append(notes, "Bad non-semver version given: "+versionValue)
243+
metrics.Notes = append(metrics.Notes, "Bad non-semver version given: "+versionValue)
248244
continue
249245
}
250246
}
@@ -279,20 +275,20 @@ func findInverseAffectedRanges(cveAff cves.Affected, cnaAssigner string) (ranges
279275
for index, f := range fixed {
280276
if index < len(introduced) {
281277
ranges = append(ranges, buildVersionRange(introduced[index], "", f))
282-
notes = append(notes, "Introduced from version value - "+introduced[index])
283-
notes = append(notes, "Fixed from version value - "+f)
278+
metrics.Notes = append(metrics.Notes, "Introduced from version value - "+introduced[index])
279+
metrics.Notes = append(metrics.Notes, "Fixed from version value - "+f)
284280
}
285281
}
286282

287283
if len(ranges) != 0 {
288-
return ranges, VersionRangeTypeSemver, notes
284+
return ranges, VersionRangeTypeSemver
289285
}
290-
notes = append(notes, "no ranges found")
286+
metrics.Notes = append(metrics.Notes, "no ranges found")
291287

292-
return nil, VersionRangeTypeUnknown, notes
288+
return nil, VersionRangeTypeUnknown
293289
}
294290

295-
func findNormalAffectedRanges(affected cves.Affected, cnaAssigner string) (versionRanges []osvschema.Range, versType VersionRangeType, notes []string) {
291+
func findNormalAffectedRanges(affected cves.Affected, cnaAssigner string, metrics *ConversionMetrics) (versionRanges []osvschema.Range, versType VersionRangeType) {
296292
versionTypesCount := make(map[VersionRangeType]int)
297293

298294
for _, vers := range affected.Versions {
@@ -308,30 +304,30 @@ func findNormalAffectedRanges(affected cves.Affected, cnaAssigner string) (versi
308304
// Quality check the version strings to avoid using filler content.
309305
vQuality := vulns.CheckQuality(vers.Version)
310306
if !vQuality.AtLeast(acceptableQuality) {
311-
notes = append(notes, fmt.Sprintf("Version value for %s %s is filler or empty", affected.Vendor, affected.Product))
307+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("Version value for %s %s is filler or empty", affected.Vendor, affected.Product))
312308
}
313309
vLessThanQual := vulns.CheckQuality(vers.LessThan)
314310
vLTOEQual := vulns.CheckQuality(vers.LessThanOrEqual)
315311

316312
hasRange := vLessThanQual.AtLeast(acceptableQuality) || vLTOEQual.AtLeast(acceptableQuality)
317-
notes = append(notes, fmt.Sprintf("Range detected: %v", hasRange))
313+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("Range detected: %v", hasRange))
318314
// Handle cases where 'lessThan' is mistakenly the same as 'version'.
319315
if vers.LessThan != "" && vers.LessThan == vers.Version {
320-
notes = append(notes, fmt.Sprintf("Warning: lessThan (%s) is the same as introduced (%s)\n", vers.LessThan, vers.Version))
316+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("Warning: lessThan (%s) is the same as introduced (%s)\n", vers.LessThan, vers.Version))
321317
hasRange = false
322318
}
323319

324320
if hasRange {
325321
if vQuality.AtLeast(acceptableQuality) {
326322
introduced = vers.Version
327-
notes = append(notes, fmt.Sprintf("%s - Introduced from version value - %s", vQuality.String(), vers.Version))
323+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("%s - Introduced from version value - %s", vQuality.String(), vers.Version))
328324
}
329325
if vLessThanQual.AtLeast(acceptableQuality) {
330326
fixed = vers.LessThan
331-
notes = append(notes, fmt.Sprintf("%s - Fixed from LessThan value - %s", vLessThanQual.String(), vers.LessThan))
327+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("%s - Fixed from LessThan value - %s", vLessThanQual.String(), vers.LessThan))
332328
} else if vLTOEQual.AtLeast(acceptableQuality) {
333329
lastaffected = vers.LessThanOrEqual
334-
notes = append(notes, fmt.Sprintf("%s - LastAffected from LessThanOrEqual value- %s", vLTOEQual.String(), vers.LessThanOrEqual))
330+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("%s - LastAffected from LessThanOrEqual value- %s", vLTOEQual.String(), vers.LessThanOrEqual))
335331
}
336332

337333
if introduced != "" && fixed != "" {
@@ -346,7 +342,7 @@ func findNormalAffectedRanges(affected cves.Affected, cnaAssigner string) (versi
346342
// In this case only vers.Version exists which either means that it is _only_ that version that is
347343
// affected, but more likely, it affects up to that version. It could also mean that the range is given
348344
// in one line instead - like "< 1.5.3" or "< 2.45.4, >= 2.0 " or just "before 1.4.7", so check for that.
349-
notes = append(notes, "Only version exists")
345+
metrics.Notes = append(metrics.Notes, "Only version exists")
350346
// GitHub often encodes the range directly in the version string.
351347
if cnaAssigner == "GitHub_M" {
352348
av, err := git.ParseVersionRange(vers.Version)
@@ -372,17 +368,17 @@ func findNormalAffectedRanges(affected cves.Affected, cnaAssigner string) (versi
372368
// Try to extract versions from text like "before 1.4.7".
373369
possibleVersions, note := cves.ExtractVersionsFromText(nil, vers.Version)
374370
if note != nil {
375-
notes = append(notes, note...)
371+
metrics.Notes = append(metrics.Notes, note...)
376372
}
377373
if possibleVersions != nil {
378-
notes = append(notes, "Versions retrieved from text but not used CURRENTLY")
374+
metrics.Notes = append(metrics.Notes, "Versions retrieved from text but not used CURRENTLY")
379375
continue
380376
}
381377

382378
// As a fallback, assume a single version means it's the fixed version.
383379
if vQuality.AtLeast(acceptableQuality) {
384380
versionRanges = append(versionRanges, buildVersionRange("0", "", vers.Version))
385-
notes = append(notes, fmt.Sprintf("%s - Single version found %v - Assuming introduced = 0 and Fixed = %v", vQuality, vers.Version, vers.Version))
381+
metrics.Notes = append(metrics.Notes, fmt.Sprintf("%s - Single version found %v - Assuming introduced = 0 and Fixed = %v", vQuality, vers.Version, vers.Version))
386382
}
387383
}
388384

@@ -396,7 +392,7 @@ func findNormalAffectedRanges(affected cves.Affected, cnaAssigner string) (versi
396392
}
397393
}
398394

399-
return versionRanges, mostFrequentVersionType, notes
395+
return versionRanges, mostFrequentVersionType
400396
}
401397

402398
// buildVersionRange is a helper function that adds 'introduced', 'fixed', or 'last_affected'

vulnfeeds/cmd/cvelist2osv/version_extraction_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestFindNormalAffectedRanges(t *testing.T) {
171171

172172
for _, tt := range tests {
173173
t.Run(tt.name, func(t *testing.T) {
174-
gotRanges, gotRangeType, _ := findNormalAffectedRanges(tt.affected, tt.cnaAssigner)
174+
gotRanges, gotRangeType := findNormalAffectedRanges(tt.affected, tt.cnaAssigner, &ConversionMetrics{})
175175
if diff := cmp.Diff(tt.wantRanges, gotRanges); diff != "" {
176176
t.Errorf("findNormalAffectedRanges() ranges mismatch (-want +got):\n%s", diff)
177177
}
@@ -278,7 +278,8 @@ func TestFindInverseAffectedRanges(t *testing.T) {
278278

279279
for _, tt := range tests {
280280
t.Run(tt.name, func(t *testing.T) {
281-
gotRanges, gotVersionType, _ := findInverseAffectedRanges(tt.affected, tt.cnaAssigner)
281+
metrics := &ConversionMetrics{}
282+
gotRanges, gotVersionType := findInverseAffectedRanges(tt.affected, tt.cnaAssigner, metrics)
282283
if diff := cmp.Diff(tt.want, gotRanges); diff != "" {
283284
t.Errorf("findInverseAffectedRanges() ranges mismatch (-want +got):\n%s", diff)
284285
}
@@ -336,7 +337,7 @@ func TestRealWorldFindInverseAffectedRanges(t *testing.T) {
336337
}
337338

338339
// Run the function under test.
339-
gotRanges, _, _ := findInverseAffectedRanges(affectedBlock, tc.cve.Metadata.AssignerShortName)
340+
gotRanges, _ := findInverseAffectedRanges(affectedBlock, tc.cve.Metadata.AssignerShortName, &ConversionMetrics{})
340341

341342
// Sort slices for deterministic comparison.
342343
sort.Slice(gotRanges, func(i, j int) bool {

vulnfeeds/cves/versions.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ func ValidateAndCanonicalizeLink(link string, httpClient *http.Client) (canonica
518518
func extractGitAffectedCommit(link string, commitType models.CommitType, httpClient *http.Client) (models.AffectedCommit, error) {
519519
var ac models.AffectedCommit
520520
c, r, err := ExtractGitCommit(link, httpClient, 0)
521+
521522
if err != nil {
522523
return ac, err
523524
}
@@ -533,6 +534,7 @@ func ExtractGitCommit(link string, httpClient *http.Client, depth int) (string,
533534
if depth > 10 {
534535
return "", "", fmt.Errorf("max recursion depth exceeded for %s", link)
535536
}
537+
536538
var commit string
537539
r, err := Repo(link)
538540
if err != nil {

0 commit comments

Comments
 (0)