-
Notifications
You must be signed in to change notification settings - Fork 187
/
main.go
712 lines (633 loc) · 24.6 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
package main
import (
"encoding/csv"
"encoding/json"
"errors"
"flag"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"golang.org/x/exp/slices"
"github.com/google/osv/vulnfeeds/cves"
"github.com/google/osv/vulnfeeds/git"
"github.com/google/osv/vulnfeeds/utility"
"github.com/google/osv/vulnfeeds/vulns"
)
type VendorProduct struct {
Vendor string
Product string
}
func (vp *VendorProduct) UnmarshalText(text []byte) error {
s := strings.Split(string(text), ":")
vp.Vendor = s[0]
vp.Product = s[1]
return nil
}
type VendorProductToRepoMap map[VendorProduct][]string
type ConversionOutcome int
var ErrNoRanges = errors.New("no ranges")
var ErrUnresolvedFix = errors.New("fixes not resolved to commits")
func (c ConversionOutcome) String() string {
return [...]string{"ConversionUnknown", "Successful", "Rejected", "NoSoftware", "NoRepos", "NoRanges", "FixUnresolvable"}[c]
}
const (
extension = ".json"
)
const (
// Set of enums for categorizing conversion outcomes.
ConversionUnknown ConversionOutcome = iota // Shouldn't happen
Successful // It worked!
Rejected // The CVE was rejected
NoSoftware // The CVE had no CPEs relating to software (i.e. Operating Systems or Hardware).
NoRepos // The CPE Vendor/Product had no repositories derived for it.
NoRanges // No viable commit ranges could be calculated from the repository for the CVE's CPE(s).
FixUnresolvable // Partial resolution of versions, resulting in a false positive.
)
var (
jsonPath = flag.String("nvd_json", "", "Path to NVD CVE JSON to examine.")
parsedCPEDictionary = flag.String("cpe_repos", "", "Path to JSON mapping of CPEs to repos generated by cpe-repo-gen")
outDir = flag.String("out_dir", "", "Path to output results.")
outFormat = flag.String("out_format", "OSV", "Format to output {OSV,PackageInfo}")
)
var Logger utility.LoggerWrapper
var RepoTagsCache git.RepoTagsCache
var Metrics struct {
TotalCVEs int
CVEsForApplications int
CVEsForKnownRepos int
OSVRecordsGenerated int
Outcomes map[cves.CVEID]ConversionOutcome // Per-CVE-ID record of conversion result.
}
// References with these tags have been found to contain completely unrelated
// repositories and can be misleading as to the software's true repository,
// Currently not used for this purpose due to undesired false positives
// reducing the number of valid records successfully converted.
var RefTagDenyList = []string{
// "Exploit",
// "Third Party Advisory",
"Broken Link", // Actively ignore these though.
}
// VendorProducts known not to be Open Source software and causing
// cross-contamination of repo derivation between CVEs.
var VendorProductDenyList = []VendorProduct{
// Causes a chain reaction of incorrect associations from CVE-2022-2068
// {"netapp", "ontap_select_deploy_administration_utility"},
// Causes misattribution for Python, e.g. CVE-2022-26488
// {"netapp", "active_iq_unified_manager"},
// Causes misattribution for OpenSSH, e.g. CVE-2021-28375
// {"netapp", "cloud_backup"},
// Three strikes and the entire netapp vendor is out...
{"netapp", ""},
// [CVE-2021-28957]: Incorrectly associates with github.com/lxml/lxml
{"oracle", "zfs_storage_appliance_kit"},
{"gradle", "enterprise"}, // The OSS repo gets mis-attributed via CVE-2020-15767
}
// Looks at what the repo to determine if it contains code using an in-scope language
func InScopeRepo(repoURL string) bool {
parsedURL, err := url.Parse(repoURL)
if err != nil {
Logger.Infof("Warning: %s failed to parse, skipping", repoURL)
return false
}
switch parsedURL.Hostname() {
case "github.com":
return InScopeGitHubRepo(repoURL)
default:
return InScopeGitRepo(repoURL)
}
}
// Use the GitHub API to query the repository's language metadata to make the determination.
func InScopeGitHubRepo(repoURL string) bool {
// TODO(apollock): Implement
return true
}
// Clone the repo and look for C/C++ files to make the determination.
func InScopeGitRepo(repoURL string) bool {
// TODO(apollock): Implement
return true
}
// Examines repos and tries to convert versions to commits by treating them as Git tags.
// Takes a CVE ID string (for logging), cves.VersionInfo with AffectedVersions and
// typically no AffectedCommits and attempts to add AffectedCommits (including Fixed commits) where there aren't any.
// Refuses to add the same commit to AffectedCommits more than once.
func GitVersionsToCommits(CVE cves.CVEID, versions cves.VersionInfo, repos []string, cache git.RepoTagsCache) (v cves.VersionInfo, e error) {
// versions is a VersionInfo with AffectedVersions and typically no AffectedCommits
// v is a VersionInfo with AffectedCommits (containing Fixed commits) included
v = versions
for _, repo := range repos {
normalizedTags, err := git.NormalizeRepoTags(repo, cache)
if err != nil {
Logger.Warnf("[%s]: Failed to normalize tags for %s: %v", CVE, repo, err)
continue
}
for _, av := range versions.AffectedVersions {
Logger.Infof("[%s]: Attempting version resolution for %+v using %q", CVE, av, repo)
introducedEquivalentCommit := ""
if av.Introduced != "" {
ac, err := git.VersionToCommit(av.Introduced, repo, cves.Introduced, normalizedTags)
if err != nil {
Logger.Warnf("[%s]: Failed to get a Git commit for introduced version %q from %q: %v", CVE, av.Introduced, repo, err)
} else {
Logger.Infof("[%s]: Successfully derived %+v for introduced version %q", CVE, ac, av.Introduced)
introducedEquivalentCommit = ac.Introduced
}
}
// Only try and convert fixed versions to commits via tags if there aren't any Fixed commits already.
// cves.ExtractVersionInfo() opportunistically returns
// AffectedCommits (with Fixed commits) when the CVE has appropriate references, and assuming these references are indeed
// Fixed commits, they're also assumed to be more precise than what may be derived from tag to commit mapping.
fixedEquivalentCommit := ""
if v.HasFixedCommits(repo) && av.Fixed != "" {
Logger.Infof("[%s]: Using preassumed fixed commits %+v instead of deriving from fixed version %q", CVE, v.FixedCommits(repo), av.Fixed)
} else if av.Fixed != "" {
ac, err := git.VersionToCommit(av.Fixed, repo, cves.Fixed, normalizedTags)
if err != nil {
Logger.Warnf("[%s]: Failed to get a Git commit for fixed version %q from %q: %v", CVE, av.Fixed, repo, err)
} else {
Logger.Infof("[%s]: Successfully derived %+v for fixed version %q", CVE, ac, av.Fixed)
fixedEquivalentCommit = ac.Fixed
}
}
// Only try and convert last_affected versions to commits via tags if there aren't any Fixed commits already (to maintain schema compliance).
// cves.ExtractVersionInfo() opportunistically returns
// AffectedCommits (with Fixed commits) when the CVE has appropriate references.
lastAffectedEquivalentCommit := ""
if !v.HasFixedCommits(repo) && av.LastAffected != "" {
ac, err := git.VersionToCommit(av.LastAffected, repo, cves.LastAffected, normalizedTags)
if err != nil {
Logger.Warnf("[%s]: Failed to get a Git commit for last_affected version %q from %q: %v", CVE, av.LastAffected, repo, err)
} else {
Logger.Infof("[%s]: Successfully derived %+v for last_affected version %q", CVE, ac, av.LastAffected)
lastAffectedEquivalentCommit = ac.LastAffected
}
}
// Assemble a single AffectedCommit from what was resolved, iff it
// doesn't result in a half-resolved (false positive-causing)
// situation with a successfully resolved introduced version and an
// unsuccessfully resolved fixed or last_affected version.
ac := cves.AffectedCommit{}
if fixedEquivalentCommit != "" || lastAffectedEquivalentCommit != "" {
ac.SetRepo(repo)
if introducedEquivalentCommit != "" {
ac.SetIntroduced(introducedEquivalentCommit)
}
ac.SetFixed(fixedEquivalentCommit)
ac.SetLastAffected(lastAffectedEquivalentCommit)
}
if ac == (cves.AffectedCommit{}) {
// Nothing resolved, move on to the next AffectedVersion
Logger.Warnf("[%s]: Sufficient resolution not possible for %+v", CVE, av)
continue
}
if ac.InvalidRange() {
Logger.Warnf("[%s]: Invalid range: %#v", CVE, ac)
continue
}
if v.Duplicated(ac) {
Logger.Warnf("[%s]: Duplicate: %#v already present in %#v", CVE, ac, v)
continue
}
v.AffectedCommits = append(v.AffectedCommits, ac)
}
}
return v, nil
}
func refAcceptable(ref cves.Reference, tagDenyList []string) bool {
for _, deniedTag := range tagDenyList {
if slices.Contains(ref.Tags, deniedTag) {
return false
}
}
return true
}
// Examines the CVE references for a CVE and derives repos for it, optionally caching it.
func ReposFromReferences(CVE string, cache VendorProductToRepoMap, vp *VendorProduct, refs []cves.Reference, tagDenyList []string) (repos []string) {
// This currently only gets called for cache misses, but make it not rely on that assumption.
if vp != nil {
if cachedRepos, ok := cache[*vp]; ok {
return cachedRepos
}
}
for _, ref := range refs {
// If any of the denylist tags are in the ref's tag set, it's out of consideration.
if !refAcceptable(ref, tagDenyList) {
// Also remove it if previously added under an acceptable tag.
maybeRemoveFromVPRepoCache(cache, vp, ref.Url)
Logger.Infof("[%s]: disregarding %q for %q due to a denied tag in %q", CVE, ref.Url, vp, ref.Tags)
continue
}
repo, err := cves.Repo(ref.Url)
if err != nil {
// Failed to parse as a valid repo.
continue
}
if !git.ValidRepo(repo) {
continue
}
if slices.Contains(repos, repo) {
continue
}
repos = append(repos, repo)
maybeUpdateVPRepoCache(cache, vp, repo)
}
return repos
}
// Takes an NVD CVE record and outputs an OSV file in the specified directory.
func CVEToOSV(CVE cves.CVE, repos []string, cache git.RepoTagsCache, directory string) error {
CPEs := cves.CPEs(CVE)
// The vendor name and product name are used to construct the output `vulnDir` below, so need to be set to *something* to keep the output tidy.
maybeVendorName := "ENOCPE"
maybeProductName := "ENOCPE"
if len(CPEs) > 0 {
CPE, err := cves.ParseCPE(CPEs[0]) // For naming the subdirectory used for output.
maybeVendorName = CPE.Vendor
maybeProductName = CPE.Product
if err != nil {
return fmt.Errorf("[%s]: Can't generate an OSV record without valid CPE data", CVE.ID)
}
}
v, notes := vulns.FromCVE(CVE.ID, CVE)
versions, versionNotes := cves.ExtractVersionInfo(CVE, nil)
notes = append(notes, versionNotes...)
if len(versions.AffectedVersions) != 0 {
var err error
// There are some AffectedVersions to try and resolve to AffectedCommits.
if len(repos) == 0 {
return fmt.Errorf("[%s]: No affected ranges for %q, and no repos to try and convert %+v to tags with", CVE.ID, maybeProductName, versions.AffectedVersions)
}
Logger.Infof("[%s]: Trying to convert version tags %+v to commits using %v", CVE.ID, versions, repos)
versions, err = GitVersionsToCommits(CVE.ID, versions, repos, cache)
if err != nil {
return fmt.Errorf("[%s]: Failed to convert version tags to commits: %#v", CVE.ID, err)
}
hasAnyFixedCommits := false
for _, repo := range repos {
if versions.HasFixedCommits(repo) {
hasAnyFixedCommits = true
break
}
}
if versions.HasFixedVersions() && !hasAnyFixedCommits {
return fmt.Errorf("[%s]: Failed to convert fixed version tags to commits: %#v %w", CVE.ID, versions, ErrUnresolvedFix)
}
hasAnyLastAffectedCommits := false
for _, repo := range repos {
if versions.HasLastAffectedCommits(repo) {
hasAnyLastAffectedCommits = true
break
}
}
if versions.HasLastAffectedVersions() && !hasAnyLastAffectedCommits && !hasAnyFixedCommits {
return fmt.Errorf("[%s]: Failed to convert last_affected version tags to commits: %#v %w", CVE.ID, versions, ErrUnresolvedFix)
}
}
slices.SortStableFunc(versions.AffectedCommits, cves.AffectedCommitCompare)
affected := vulns.Affected{}
affected.AttachExtractedVersionInfo(versions)
v.Affected = append(v.Affected, affected)
if len(v.Affected[0].Ranges) == 0 {
return fmt.Errorf("[%s]: No affected ranges detected for %q %w", CVE.ID, maybeProductName, ErrNoRanges)
}
vulnDir := filepath.Join(directory, maybeVendorName, maybeProductName)
err := os.MkdirAll(vulnDir, 0755)
if err != nil {
Logger.Warnf("Failed to create dir: %v", err)
return fmt.Errorf("failed to create dir: %v", err)
}
outputFile := filepath.Join(vulnDir, v.ID+extension)
notesFile := filepath.Join(vulnDir, v.ID+".notes")
f, err := os.Create(outputFile)
if err != nil {
Logger.Warnf("Failed to open %s for writing: %v", outputFile, err)
return fmt.Errorf("failed to open %s for writing: %v", outputFile, err)
}
defer f.Close()
err = v.ToJSON(f)
if err != nil {
Logger.Warnf("Failed to write %s: %v", outputFile, err)
return fmt.Errorf("failed to write %s: %v", outputFile, err)
}
Logger.Infof("[%s]: Generated OSV record for %q", CVE.ID, maybeProductName)
if len(notes) > 0 {
err = os.WriteFile(notesFile, []byte(strings.Join(notes, "\n")), 0660)
if err != nil {
Logger.Warnf("[%s]: Failed to write %s: %v", CVE.ID, notesFile, err)
}
}
return nil
}
// Takes an NVD CVE record and outputs a PackageInfo struct in a file in the specified directory.
func CVEToPackageInfo(CVE cves.CVE, repos []string, cache git.RepoTagsCache, directory string) error {
CPEs := cves.CPEs(CVE)
// The vendor name and product name are used to construct the output `vulnDir` below, so need to be set to *something* to keep the output tidy.
maybeVendorName := "ENOCPE"
maybeProductName := "ENOCPE"
if len(CPEs) > 0 {
CPE, err := cves.ParseCPE(CPEs[0]) // For naming the subdirectory used for output.
maybeVendorName = CPE.Vendor
maybeProductName = CPE.Product
if err != nil {
return fmt.Errorf("[%s]: Can't generate an OSV record without valid CPE data", CVE.ID)
}
}
// more often than not, this yields a VersionInfo with AffectedVersions and no AffectedCommits.
versions, notes := cves.ExtractVersionInfo(CVE, nil)
if len(versions.AffectedVersions) != 0 {
var err error
// There are some AffectedVersions to try and resolve to AffectedCommits.
if len(repos) == 0 {
return fmt.Errorf("[%s]: No affected ranges for %q, and no repos to try and convert %+v to tags with", CVE.ID, maybeProductName, versions.AffectedVersions)
}
Logger.Infof("[%s]: Trying to convert version tags %+v to commits using %v", CVE.ID, versions, repos)
versions, err = GitVersionsToCommits(CVE.ID, versions, repos, cache)
if err != nil {
return fmt.Errorf("[%s]: Failed to convert version tags to commits: %#v", CVE.ID, err)
}
}
hasAnyFixedCommits := false
for _, repo := range repos {
if versions.HasFixedCommits(repo) {
hasAnyFixedCommits = true
}
}
if versions.HasFixedVersions() && !hasAnyFixedCommits {
return fmt.Errorf("[%s]: Failed to convert fixed version tags to commits: %#v %w", CVE.ID, versions, ErrUnresolvedFix)
}
hasAnyLastAffectedCommits := false
for _, repo := range repos {
if versions.HasLastAffectedCommits(repo) {
hasAnyLastAffectedCommits = true
}
}
if versions.HasLastAffectedVersions() && !hasAnyLastAffectedCommits && !hasAnyFixedCommits {
return fmt.Errorf("[%s]: Failed to convert last_affected version tags to commits: %#v %w", CVE.ID, versions, ErrUnresolvedFix)
}
if len(versions.AffectedCommits) == 0 {
return fmt.Errorf("[%s]: No affected commit ranges determined for %q %w", CVE.ID, maybeProductName, ErrNoRanges)
}
versions.AffectedVersions = nil // these have served their purpose and are not required in the resulting output.
slices.SortStableFunc(versions.AffectedCommits, cves.AffectedCommitCompare)
var pkgInfos []vulns.PackageInfo
pi := vulns.PackageInfo{VersionInfo: versions}
pkgInfos = append(pkgInfos, pi) // combine-to-osv expects a serialised *array* of PackageInfo
vulnDir := filepath.Join(directory, maybeVendorName, maybeProductName)
err := os.MkdirAll(vulnDir, 0755)
if err != nil {
Logger.Warnf("Failed to create dir: %v", err)
return fmt.Errorf("failed to create dir: %v", err)
}
outputFile := filepath.Join(vulnDir, string(CVE.ID)+".nvd"+extension)
notesFile := filepath.Join(vulnDir, string(CVE.ID)+".nvd.notes")
f, err := os.Create(outputFile)
if err != nil {
Logger.Warnf("Failed to open %s for writing: %v", outputFile, err)
return fmt.Errorf("failed to open %s for writing: %v", outputFile, err)
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
err = encoder.Encode(&pkgInfos)
if err != nil {
Logger.Warnf("Failed to encode PackageInfo to %s: %v", outputFile, err)
return fmt.Errorf("failed to encode PackageInfo to %s: %v", outputFile, err)
}
Logger.Infof("[%s]: Generated PackageInfo record for %q", CVE.ID, maybeProductName)
if len(notes) > 0 {
err = os.WriteFile(notesFile, []byte(strings.Join(notes, "\n")), 0660)
if err != nil {
Logger.Warnf("[%s]: Failed to write %s: %v", CVE.ID, notesFile, err)
}
}
return nil
}
func loadCPEDictionary(ProductToRepo *VendorProductToRepoMap, f string) error {
data, err := os.ReadFile(f)
if err != nil {
return err
}
return json.Unmarshal(data, &ProductToRepo)
}
// Adds the repo to the cache for the Vendor/Product combination if not already present.
func maybeUpdateVPRepoCache(cache VendorProductToRepoMap, vp *VendorProduct, repo string) {
if vp == nil {
return
}
if slices.Contains(cache[*vp], repo) {
return
}
cache[*vp] = append(cache[*vp], repo)
}
// Removes the repo from the cache for the Vendor/Product combination if already present.
func maybeRemoveFromVPRepoCache(cache VendorProductToRepoMap, vp *VendorProduct, repo string) {
if vp == nil {
return
}
cacheEntry, ok := cache[*vp]
if !ok {
return
}
if !slices.Contains(cacheEntry, repo) {
return
}
i := slices.Index(cacheEntry, repo)
if i == -1 {
return
}
// If there is only one entry, delete the entry cache entry.
if len(cacheEntry) == 1 {
delete(cache, *vp)
return
}
cacheEntry = slices.Delete(cacheEntry, i, i+1)
cache[*vp] = cacheEntry
}
// Output a CSV summarizing per-CVE how it was handled.
func outputOutcomes(outcomes map[cves.CVEID]ConversionOutcome, reposForCVE map[cves.CVEID][]string, directory string) error {
outcomesFile, err := os.Create(filepath.Join(directory, "outcomes.csv"))
if err != nil {
return err
}
defer outcomesFile.Close()
w := csv.NewWriter(outcomesFile)
w.Write([]string{"CVE", "outcome", "repos"})
for CVE, outcome := range outcomes {
// It's conceivable to have more than one repo for a CVE, so concatenate them.
r := ""
if repos, ok := reposForCVE[CVE]; ok {
r = strings.Join(repos, " ")
}
w.Write([]string{string(CVE), outcome.String(), r})
}
w.Flush()
if err = w.Error(); err != nil {
return err
}
return nil
}
func main() {
flag.Parse()
if !slices.Contains([]string{"OSV", "PackageInfo"}, *outFormat) {
fmt.Fprintf(os.Stderr, "Unsupported output format: %s\n", *outFormat)
os.Exit(1)
}
Metrics.Outcomes = make(map[cves.CVEID]ConversionOutcome)
var logCleanup func()
Logger, logCleanup = utility.CreateLoggerWrapper("cpp-osv")
defer logCleanup()
data, err := os.ReadFile(*jsonPath)
if err != nil {
Logger.Fatalf("Failed to open file: %v", err) // double check this is best practice output
}
var parsed cves.CVEAPIJSON20Schema
err = json.Unmarshal(data, &parsed)
if err != nil {
Logger.Fatalf("Failed to parse NVD CVE JSON: %v", err)
}
VPRepoCache := make(VendorProductToRepoMap)
if *parsedCPEDictionary != "" {
err = loadCPEDictionary(&VPRepoCache, *parsedCPEDictionary)
if err != nil {
Logger.Fatalf("Failed to load parsed CPE dictionary: %v", err)
}
Logger.Infof("VendorProductToRepoMap cache has %d entries preloaded", len(VPRepoCache))
}
ReposForCVE := make(map[cves.CVEID][]string)
for _, cve := range parsed.Vulnerabilities {
refs := cve.CVE.References
CPEs := cves.CPEs(cve.CVE)
CVEID := cve.CVE.ID
if len(refs) == 0 && len(CPEs) == 0 {
Logger.Infof("[%s]: skipping due to lack of CPEs and lack of references", CVEID)
// 100% of these in 2022 were rejected CVEs
Metrics.Outcomes[CVEID] = Rejected
continue
}
// Edge case: No CPEs, but perhaps usable references.
if len(refs) > 0 && len(CPEs) == 0 {
repos := ReposFromReferences(string(CVEID), nil, nil, refs, RefTagDenyList)
if len(repos) == 0 {
Logger.Warnf("[%s]: Failed to derive any repos and there were no CPEs", CVEID)
continue
}
Logger.Infof("[%s]: Derived %q for CVE with no CPEs", CVEID, repos)
ReposForCVE[CVEID] = repos
}
// Does it have any application CPEs? Look for pre-computed repos based on VendorProduct.
appCPECount := 0
for _, CPEstr := range cves.CPEs(cve.CVE) {
CPE, err := cves.ParseCPE(CPEstr)
if err != nil {
Logger.Warnf("[%s]: Failed to parse CPE %q: %+v", CVEID, CPEstr, err)
Metrics.Outcomes[CVEID] = ConversionUnknown
continue
}
if CPE.Part == "a" {
appCPECount += 1
}
if _, ok := VPRepoCache[VendorProduct{CPE.Vendor, CPE.Product}]; ok {
Logger.Infof("[%s]: Pre-references, derived %q for %q %q using cache", CVEID, VPRepoCache[VendorProduct{CPE.Vendor, CPE.Product}], CPE.Vendor, CPE.Product)
if _, ok := ReposForCVE[CVEID]; !ok {
ReposForCVE[CVEID] = VPRepoCache[VendorProduct{CPE.Vendor, CPE.Product}]
continue
}
// Don't append duplicates.
for _, repo := range VPRepoCache[VendorProduct{CPE.Vendor, CPE.Product}] {
if !slices.Contains(ReposForCVE[CVEID], repo) {
ReposForCVE[CVEID] = append(ReposForCVE[CVEID], repo)
}
}
}
}
if len(CPEs) > 0 && appCPECount == 0 {
// This CVE is not for software (based on there being CPEs but not any application ones), skip.
Metrics.Outcomes[CVEID] = NoSoftware
continue
}
if appCPECount > 0 {
Metrics.CVEsForApplications++
}
// If there wasn't a repo from the CPE Dictionary, try and derive one from the CVE references.
if _, ok := ReposForCVE[CVEID]; !ok && len(refs) > 0 {
for _, CPEstr := range cves.CPEs(cve.CVE) {
CPE, err := cves.ParseCPE(CPEstr)
if err != nil {
Logger.Warnf("[%s]: Failed to parse CPE %q: %+v", CVEID, CPEstr, err)
continue
}
// Continue to only focus on application CPEs.
if CPE.Part != "a" {
continue
}
if slices.Contains(VendorProductDenyList, VendorProduct{CPE.Vendor, ""}) {
continue
}
if slices.Contains(VendorProductDenyList, VendorProduct{CPE.Vendor, CPE.Product}) {
continue
}
repos := ReposFromReferences(string(CVEID), VPRepoCache, &VendorProduct{CPE.Vendor, CPE.Product}, refs, RefTagDenyList)
if len(repos) == 0 {
Logger.Warnf("[%s]: Failed to derive any repos for %q %q", CVEID, CPE.Vendor, CPE.Product)
continue
}
Logger.Infof("[%s]: Derived %q for %q %q", CVEID, repos, CPE.Vendor, CPE.Product)
ReposForCVE[CVEID] = repos
}
}
Logger.Infof("[%s]: Summary: [CPEs=%d AppCPEs=%d DerivedRepos=%d]", CVEID, len(CPEs), appCPECount, len(ReposForCVE[CVEID]))
// If we've made it to here, we may have a CVE:
// * that has Application-related CPEs (so applies to software)
// * has a reference that is a known repository URL
// OR
// * a derived repository for the software package
//
// We do not yet have:
// * any knowledge of the language used
// * definitive version information
if _, ok := ReposForCVE[CVEID]; !ok {
// We have nothing useful to work with, so we'll assume it's out of scope
Logger.Infof("[%s]: Passing due to lack of viable repository", CVEID)
Metrics.Outcomes[CVEID] = NoRepos
continue
}
Logger.Infof("[%s]: Repos: %#v", CVEID, ReposForCVE[CVEID])
for _, repo := range ReposForCVE[CVEID] {
if !InScopeRepo(repo) {
continue
}
}
Metrics.CVEsForKnownRepos++
switch *outFormat {
case "OSV":
err = CVEToOSV(cve.CVE, ReposForCVE[CVEID], RepoTagsCache, *outDir)
case "PackageInfo":
err = CVEToPackageInfo(cve.CVE, ReposForCVE[CVEID], RepoTagsCache, *outDir)
}
// Parse this error to determine which failure mode it was
if err != nil {
Logger.Warnf("[%s]: Failed to generate an OSV record: %+v", CVEID, err)
if errors.Is(err, ErrNoRanges) {
Metrics.Outcomes[CVEID] = NoRanges
continue
}
if errors.Is(err, ErrUnresolvedFix) {
Metrics.Outcomes[CVEID] = FixUnresolvable
continue
}
Metrics.Outcomes[CVEID] = ConversionUnknown
continue
}
Metrics.OSVRecordsGenerated++
Metrics.Outcomes[CVEID] = Successful
}
Metrics.TotalCVEs = len(parsed.Vulnerabilities)
err = outputOutcomes(Metrics.Outcomes, ReposForCVE, *outDir)
if err != nil {
// Log entry with size 1.15M exceeds maximum size of 256.0K
fmt.Fprintf(os.Stderr, "Failed to write out metrics: %v", err)
}
// Outcomes is too big to log, so zero it out.
Metrics.Outcomes = nil
Logger.Infof("%s Metrics: %+v", filepath.Base(*jsonPath), Metrics)
}