From 289c0ebc928e363e50398f109d95c38236f2a947 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 9 Jul 2026 01:23:31 +0000 Subject: [PATCH 01/13] combine-to-osv go vroom --- vulnfeeds/cmd/combine-to-osv/main.go | 434 ++++++++++++++++------ vulnfeeds/cmd/combine-to-osv/main_test.go | 82 +++- 2 files changed, 394 insertions(+), 122 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index e1c97a81c07..ba59d4b3184 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -7,16 +7,20 @@ import ( "errors" "flag" "fmt" + "io" "io/fs" "log/slog" "os" "path/filepath" "slices" "strings" + "sync" + "sync/atomic" "cloud.google.com/go/storage" "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/conversion/writer" + "github.com/google/osv/vulnfeeds/gcs-tools" "github.com/google/osv/vulnfeeds/models" "github.com/google/osv/vulnfeeds/utility" "github.com/google/osv/vulnfeeds/utility/logger" @@ -32,6 +36,149 @@ const ( defaultNVDOSVPath = "nvd" ) +// CVEWorkItem represents a unit of work for a single CVE. +type CVEWorkItem struct { + ID models.CVEID + CVE5Path string + NVDPath string +} + +func cveIDFromPath(p string) models.CVEID { + base := filepath.Base(p) + id := strings.TrimSuffix(base, ".json") + if strings.HasPrefix(id, "CVE-") { + return models.CVEID(id) + } + + return "" +} + +func listObjects(ctx context.Context, client *storage.Client, pathStr string) ([]string, error) { + if strings.HasPrefix(pathStr, "gs://") { + trimmed := strings.TrimPrefix(pathStr, "gs://") + bucketName, prefix, _ := strings.Cut(trimmed, "/") + bucket := client.Bucket(bucketName) + objs, err := gcs.ListBucketObjects(ctx, bucket, prefix) + if err != nil { + return nil, err + } + var fullPaths []string + for _, obj := range objs { + if strings.HasSuffix(obj, "/") || !strings.HasSuffix(obj, ".json") || strings.HasSuffix(obj, ".metrics.json") { + continue + } + fullPaths = append(fullPaths, fmt.Sprintf("gs://%s/%s", bucketName, obj)) + } + + return fullPaths, nil + } + + var files []string + err := filepath.WalkDir(pathStr, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && strings.HasSuffix(p, ".json") && !strings.HasSuffix(p, ".metrics.json") { + files = append(files, p) + } + + return nil + }) + + return files, err +} + +func readVulnerability(ctx context.Context, client *storage.Client, fullPath string) (*osvschema.Vulnerability, error) { + if strings.HasPrefix(fullPath, "gs://") { + trimmed := strings.TrimPrefix(fullPath, "gs://") + bucketName, objName, _ := strings.Cut(trimmed, "/") + rc, err := client.Bucket(bucketName).Object(objName).NewReader(ctx) + if err != nil { + return nil, err + } + defer rc.Close() + file, err := io.ReadAll(rc) + if err != nil { + return nil, err + } + var vuln osvschema.Vulnerability + if err := protojson.Unmarshal(file, &vuln); err != nil { + return nil, err + } + + return &vuln, nil + } + + file, err := os.ReadFile(fullPath) + if err != nil { + return nil, err + } + var vuln osvschema.Vulnerability + if err := protojson.Unmarshal(file, &vuln); err != nil { + return nil, err + } + + return &vuln, nil +} + +func combineOneOSVRecord(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability, mandatoryCVEIDs []string) *osvschema.Vulnerability { + var baseOSV *osvschema.Vulnerability + if cve5 != nil && nvd != nil { + baseOSV = combineTwoOSVRecords(cve5, nvd) + } else if cve5 != nil { + baseOSV = cve5 + } else if nvd != nil { + baseOSV = nvd + } else { + return nil + } + + if len(baseOSV.GetAffected()) == 0 || !hasRanges(baseOSV.GetAffected()) { + if !slices.Contains(mandatoryCVEIDs, string(cveID)) { + return nil + } + } + + return baseOSV +} + +func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan <-chan *CVEWorkItem, vulnChan chan<- *osvschema.Vulnerability, mandatoryCVEIDs []string) { + for work := range workChan { + var cve5, nvd *osvschema.Vulnerability + var cve5Err, nvdErr error + var readWg sync.WaitGroup + + if work.CVE5Path != "" { + readWg.Add(1) + go func() { + defer readWg.Done() + cve5, cve5Err = readVulnerability(ctx, client, work.CVE5Path) + if cve5Err != nil { + logger.Warn("Failed to read CVE5", slog.String("id", string(work.ID)), slog.Any("err", cve5Err)) + } + }() + } + + if work.NVDPath != "" { + readWg.Add(1) + go func() { + defer readWg.Done() + nvd, nvdErr = readVulnerability(ctx, client, work.NVDPath) + if nvdErr != nil { + logger.Warn("Failed to read NVD", slog.String("id", string(work.ID)), slog.Any("err", nvdErr)) + } + }() + } + + readWg.Wait() + + combined := combineOneOSVRecord(work.ID, cve5, nvd, mandatoryCVEIDs) + if combined != nil { + vulnChan <- combined + } + } +} + func main() { logger.InitGlobalLogger() defer logger.Close() @@ -51,50 +198,190 @@ func main() { logger.Fatal("Can't create output path", slog.Any("err", err)) } - // Load CVE5 OSVs - allCVE5 := loadOSV(*cve5Path) - // Load NVD OSVs - allNVD := loadOSV(*nvdPath) - debianCVEs, err := listBucketObjects("osv-test-debian-osv", "/debian-cve-osv") + ctx := context.Background() + client, err := storage.NewClient(ctx) if err != nil { - logger.Warn("Failed to list debian cves", slog.Any("err", err)) - } else { - for i, filename := range debianCVEs { - cve := extractCVEName(filename, "DEBIAN-") - if cve != "" { - debianCVEs[i] = cve + logger.Fatal("Failed to create storage client", slog.Any("err", err)) + } + defer client.Close() + + var debianCVEs, alpineCVEs []string + var debianErr, alpineErr error + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + debianCVEs, debianErr = listBucketObjects(ctx, client, "osv-test-debian-osv", "/debian-cve-osv") + if debianErr != nil { + logger.Warn("Failed to list debian cves", slog.Any("err", debianErr)) + } else { + for i, filename := range debianCVEs { + cve := extractCVEName(filename, "DEBIAN-") + if cve != "" { + debianCVEs[i] = cve + } } } + }() + + go func() { + defer wg.Done() + alpineCVEs, alpineErr = listBucketObjects(ctx, client, "osv-test-cve-osv-conversion", "/alpine") + if alpineErr != nil { + logger.Warn("Failed to list alpine cves", slog.Any("err", alpineErr)) + } else { + for i, filename := range alpineCVEs { + cve := extractCVEName(filename, "ALPINE-") + if cve != "" { + alpineCVEs[i] = cve + } + } + } + }() + + wg.Wait() + + mandatoryCVEIDs := append(debianCVEs, alpineCVEs...) //nolint:gocritic + + // List CVE5 and NVD objects + var cve5Files, nvdFiles []string + var cve5ListErr, nvdListErr error + var listWg sync.WaitGroup + listWg.Add(2) + + go func() { + defer listWg.Done() + logger.Info("Listing CVE5 objects", slog.String("path", *cve5Path)) + cve5Files, cve5ListErr = listObjects(ctx, client, *cve5Path) + if cve5ListErr != nil { + logger.Fatal("Failed to list CVE5 objects", slog.Any("err", cve5ListErr)) + } + }() + + go func() { + defer listWg.Done() + logger.Info("Listing NVD objects", slog.String("path", *nvdPath)) + nvdFiles, nvdListErr = listObjects(ctx, client, *nvdPath) + if nvdListErr != nil { + logger.Fatal("Failed to list NVD objects", slog.Any("err", nvdListErr)) + } + }() + + listWg.Wait() + + // Build work items + workItems := make(map[models.CVEID]*CVEWorkItem) + for _, f := range cve5Files { + id := cveIDFromPath(f) + if id != "" { + if _, ok := workItems[id]; !ok { + workItems[id] = &CVEWorkItem{ID: id} + } + workItems[id].CVE5Path = f + } } - // run extract file name on each element in debianCVEs and alpineCVEs. - alpineCVEs, err := listBucketObjects("osv-test-cve-osv-conversion", "/alpine") - if err != nil { - logger.Warn("Failed to list alpine cves", slog.Any("err", err)) - } else { - for i, filename := range alpineCVEs { - cve := extractCVEName(filename, "ALPINE-") - if cve != "" { - alpineCVEs[i] = cve + for _, f := range nvdFiles { + id := cveIDFromPath(f) + if id != "" { + if _, ok := workItems[id]; !ok { + workItems[id] = &CVEWorkItem{ID: id} } + workItems[id].NVDPath = f } } - // this ensures the creation of CVEs even if they don't have packages - // to ensure Alpine and Debian CVEs have an upstream CVE. - // linter is compaining that we aren't appending to the same slice, but we - // just want to combine these two arrays with a more descriptive name. - mandatoryCVEIDs := append(debianCVEs, alpineCVEs...) //nolint:gocritic - combinedData := combineIntoOSV(allCVE5, allNVD, mandatoryCVEIDs) + logger.Info("Total CVE Work Items to process", slog.Int("count", len(workItems))) - ctx := context.Background() + // Start Upload Pool + var outBkt, overridesBkt *storage.BucketHandle + var gcsHelper *gcs.Helper + if *uploadToGCS { + outBkt = client.Bucket(*outputBucketName) + if *overridesBucketName != "" { + overridesBkt = client.Bucket(*overridesBucketName) + } + gcsHelper, err = gcs.InitUploadPool(ctx, *numWorkers, *outputBucketName) + if err != nil { + logger.Fatal("Failed to initialize GCS upload pool", slog.Any("err", err)) + } + defer gcsHelper.CloseAndWait() + } + + // Start Channels + vulnChan := make(chan *osvschema.Vulnerability, *numWorkers) + validVulnChan := make(chan *osvschema.Vulnerability, *numWorkers) + workChan := make(chan *CVEWorkItem, *numWorkers) + + // Start VulnWorkers (Upload side) + var uploadWg sync.WaitGroup + var successCount atomic.Uint64 + for range *numWorkers { + uploadWg.Add(1) + go func() { + defer uploadWg.Done() + writer.VulnWorker(ctx, validVulnChan, outBkt, overridesBkt, gcsHelper, *osvOutputPath, &successCount) + }() + } + + // Interpose Collector to gather valid IDs + var validIDs []string + var idWg sync.WaitGroup + idWg.Add(1) + totalWork := len(workItems) + go func() { + defer idWg.Done() + count := 0 + for v := range vulnChan { + count++ + if len(v.GetAffected()) > 0 { + validIDs = append(validIDs, v.GetId()) + } + validVulnChan <- v + if count%1000 == 0 { + logger.Info("Processed CVEs", slog.Int("count", count), slog.Int("total", totalWork), slog.Int("percent", (count*100)/totalWork)) + } + } + close(validVulnChan) + }() + + // Start ReadAndCombineWorkers (Read side) + var readWg sync.WaitGroup + for range *numWorkers { + readWg.Add(1) + go func() { + defer readWg.Done() + readAndCombineWorker(ctx, client, workChan, vulnChan, mandatoryCVEIDs) + }() + } + + // Feed Work + go func() { + for _, work := range workItems { + workChan <- work + } + close(workChan) + }() + + // Wait for reads to finish + readWg.Wait() + close(vulnChan) + + // Wait for collector and uploads to finish + idWg.Wait() + uploadWg.Wait() - vulnerabilities := make([]*osvschema.Vulnerability, 0, len(combinedData)) - for _, v := range combinedData { - vulnerabilities = append(vulnerabilities, v) + logger.Info("Successfully processed OSV files", slog.Int("count", len(validIDs))) + if outBkt == nil && gcsHelper == nil { + logger.Info("Successfully wrote records to disk", slog.Uint64("count", successCount.Load())) } - writer.UploadVulnsToGCS(ctx, "OSV files", *uploadToGCS, *outputBucketName, *overridesBucketName, *numWorkers, *osvOutputPath, vulnerabilities, *syncDeletions) + // Handle Deletion + if *syncDeletions && *uploadToGCS { + writer.HandleDeletion(ctx, outBkt, *osvOutputPath, validIDs) + } } // extractCVEName extracts the CVE name from a given filename and prefix. @@ -112,13 +399,7 @@ func extractCVEName(filename string, prefix string) string { // listBucketObjects lists the names of all objects in a Google Cloud Storage bucket. // It does not download the file contents. -func listBucketObjects(bucketName string, prefix string) ([]string, error) { - ctx := context.Background() - client, err := storage.NewClient(ctx) - if err != nil { - return nil, fmt.Errorf("storage.NewClient: %w", err) - } - defer client.Close() +func listBucketObjects(ctx context.Context, client *storage.Client, bucketName string, prefix string) ([]string, error) { bucket := client.Bucket(bucketName) it := bucket.Objects(ctx, &storage.Query{Prefix: prefix}) var filenames []string @@ -136,83 +417,6 @@ func listBucketObjects(bucketName string, prefix string) ([]string, error) { return filenames, nil } -// loadOSV recursively loads all OSV vulnerabilities from a given directory path. -// It walks the directory, reads each ".json" file, and decodes it into an osvschema.Vulnerability object. -// The function returns a map of CVE IDs to their corresponding Vulnerability objects. -// Files that are not ".json" files, directories, or files ending in ".metrics.json" are skipped. -// The function will log warnings for files that fail to open or decode, and will terminate if it fails to walk the directory. -func loadOSV(osvPath string) map[models.CVEID]*osvschema.Vulnerability { - allVulns := make(map[models.CVEID]*osvschema.Vulnerability) - logger.Info("Loading OSV records", slog.String("path", osvPath)) - err := filepath.WalkDir(osvPath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() || !strings.HasSuffix(path, ".json") || strings.HasSuffix(path, ".metrics.json") { - return nil - } - - file, err := os.ReadFile(path) - if err != nil { - logger.Warn("Failed to open OSV JSON file", slog.String("path", path), slog.Any("err", err)) - return nil - } - - var vuln osvschema.Vulnerability - decodeErr := protojson.Unmarshal(file, &vuln) - if decodeErr != nil { - logger.Error("Failed to decode, skipping", slog.String("file", path), slog.Any("err", decodeErr)) - return nil - } - allVulns[models.CVEID(vuln.GetId())] = &vuln - - return nil - }) - - if err != nil { - logger.Fatal("Failed to walk OSV directory", slog.String("path", osvPath), slog.Any("err", err)) - } - - return allVulns -} - -// combineIntoOSV creates OSV entry by combining loaded CVEs from NVD and PackageInfo information from security advisories. -func combineIntoOSV(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability, mandatoryCVEIDs []string) map[models.CVEID]*osvschema.Vulnerability { - osvRecords := make(map[models.CVEID]*osvschema.Vulnerability) - - // Iterate through CVEs from security advisories (cve5) as the base - for cveID, cve5 := range cve5osv { - var baseOSV *osvschema.Vulnerability - nvd, ok := nvdosv[cveID] - - if ok { - baseOSV = combineTwoOSVRecords(cve5, nvd) - // The CVE is processed, so remove it from the nvdosv map to avoid re-processing. - delete(nvdosv, cveID) - } else { - baseOSV = cve5 - } - - if len(baseOSV.GetAffected()) == 0 || !hasRanges(baseOSV.GetAffected()) { - // check if part exists. - if !slices.Contains(mandatoryCVEIDs, string(cveID)) { - continue - } - } - osvRecords[cveID] = baseOSV - } - - // Add any remaining CVEs from NVD that were not in the advisory data. - for cveID, nvd := range nvdosv { - if len(nvd.GetAffected()) == 0 || !hasRanges(nvd.GetAffected()) { - continue - } - osvRecords[cveID] = nvd - } - - return osvRecords -} - // combineTwoOSVRecords takes two osv records and combines them into one func combineTwoOSVRecords(cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { baseOSV := cve5 diff --git a/vulnfeeds/cmd/combine-to-osv/main_test.go b/vulnfeeds/cmd/combine-to-osv/main_test.go index aaf0139f004..0a1e7f5e1c5 100644 --- a/vulnfeeds/cmd/combine-to-osv/main_test.go +++ b/vulnfeeds/cmd/combine-to-osv/main_test.go @@ -1,15 +1,22 @@ package main import ( + "io/fs" + "log/slog" + "os" "path/filepath" + "slices" "sort" + "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/osv/vulnfeeds/models" + "github.com/google/osv/vulnfeeds/utility/logger" "github.com/ossf/osv-schema/bindings/go/osvschema" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -17,17 +24,78 @@ import ( const testdataPath = "../../test_data/combine-to-osv" -func TestLoadOSV(t *testing.T) { - cve5Path := filepath.Join(testdataPath, "cve5") - allVulns := loadOSV(cve5Path) +// loadOSV loads OSV vulnerabilities from a given local directory path. +// It returns a map of CVE IDs to their corresponding Vulnerability objects. +func loadOSV(osvPath string) map[models.CVEID]*osvschema.Vulnerability { + allVulns := make(map[models.CVEID]*osvschema.Vulnerability) + logger.Info("Loading OSV records from local path", slog.String("path", osvPath)) + err := filepath.WalkDir(osvPath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".json") || strings.HasSuffix(path, ".metrics.json") { + return nil + } + + file, err := os.ReadFile(path) + if err != nil { + logger.Warn("Failed to open OSV JSON file", slog.String("path", path), slog.Any("err", err)) + return nil + } + + var vuln osvschema.Vulnerability + decodeErr := protojson.Unmarshal(file, &vuln) + if decodeErr != nil { + logger.Error("Failed to decode, skipping", slog.String("file", path), slog.Any("err", decodeErr)) + return nil + } + allVulns[models.CVEID(vuln.GetId())] = &vuln + + return nil + }) + + if err != nil { + logger.Fatal("Failed to walk OSV directory", slog.String("path", osvPath), slog.Any("err", err)) + } - if len(allVulns) != 4 { - t.Errorf("Expected 4 vulnerabilities, got %d", len(allVulns)) + return allVulns +} + +// combineIntoOSV creates OSV entry by combining loaded CVEs from NVD and PackageInfo information from security advisories. +func combineIntoOSV(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability, mandatoryCVEIDs []string) map[models.CVEID]*osvschema.Vulnerability { + osvRecords := make(map[models.CVEID]*osvschema.Vulnerability) + + // Iterate through CVEs from security advisories (cve5) as the base + for cveID, cve5 := range cve5osv { + var baseOSV *osvschema.Vulnerability + nvd, ok := nvdosv[cveID] + + if ok { + baseOSV = combineTwoOSVRecords(cve5, nvd) + // The CVE is processed, so remove it from the nvdosv map to avoid re-processing. + delete(nvdosv, cveID) + } else { + baseOSV = cve5 + } + + if len(baseOSV.GetAffected()) == 0 || !hasRanges(baseOSV.GetAffected()) { + // check if part exists. + if !slices.Contains(mandatoryCVEIDs, string(cveID)) { + continue + } + } + osvRecords[cveID] = baseOSV } - if _, ok := allVulns["CVE-2023-1234"]; !ok { - t.Error("Expected to load CVE-2023-1234") + // Add any remaining CVEs from NVD that were not in the advisory data. + for cveID, nvd := range nvdosv { + if len(nvd.GetAffected()) == 0 || !hasRanges(nvd.GetAffected()) { + continue + } + osvRecords[cveID] = nvd } + + return osvRecords } func TestCombineIntoOSV(t *testing.T) { From 04c04e379d93874003d62b228c0e19fa92548702 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 9 Jul 2026 01:27:29 +0000 Subject: [PATCH 02/13] missing files --- .../run_combine_to_osv_convert.sh | 20 ++++--------------- vulnfeeds/conversion/writer/writer.go | 12 +++++++---- vulnfeeds/conversion/writer/writer_test.go | 7 ++----- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh b/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh index 3330d96af00..908547c1216 100755 --- a/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh +++ b/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh @@ -16,31 +16,19 @@ trap '__error_handing__ $? $LINENO' ERR set -eu -INPUT_BUCKET="${INPUT_GCS_BUCKET:=cve-osv-conversion}" -OUTPUT_BUCKET="${OUTPUT_GCS_BUCKET:=cve-osv-conversion}" +INPUT_BUCKET="${INPUT_GCS_BUCKET:=osv-test-cve-osv-conversion}" +OUTPUT_BUCKET="${OUTPUT_GCS_BUCKET:=osv-test-cve-osv-conversion}" NUM_WORKERS="${NUM_WORKERS:=64}" OSV_OUTPUT="osv-output" -NVD_OSV_OUTPUT="nvd" -CVE5_OSV_OUTPUT="cve5" echo "Setup initial directories" -rm -rf $NVD_OSV_OUTPUT && mkdir -p $NVD_OSV_OUTPUT rm -rf $OSV_OUTPUT && mkdir -p $OSV_OUTPUT -rm -rf $CVE5_OSV_OUTPUT && mkdir -p $CVE5_OSV_OUTPUT - -echo "Begin syncing NVD data from GCS bucket ${INPUT_BUCKET}" -gcloud --no-user-output-enabled storage -q cp "gs://${INPUT_BUCKET}/nvd-osv/CVE-????-*.json" "${NVD_OSV_OUTPUT}" -echo "Successfully synced from GCS bucket" - -echo "Begin syncing CVE5 data from GCS bucket ${INPUT_BUCKET}" -gcloud --no-user-output-enabled storage -q cp "gs://${INPUT_BUCKET}/cve5/CVE-????-*.json" "${CVE5_OSV_OUTPUT}" -echo "Successfully synced from GCS bucket" echo "Run combine-to-osv" ./combine-to-osv \ - -cve5-path "$CVE5_OSV_OUTPUT" \ - -nvd-path "$NVD_OSV_OUTPUT" \ + -cve5-path "gs://${INPUT_BUCKET}/cve5/" \ + -nvd-path "gs://${INPUT_BUCKET}/nvd-osv/" \ -osv-output-path "$OSV_OUTPUT" \ -upload-to-gcs \ -output-bucket "${OUTPUT_BUCKET}" \ diff --git a/vulnfeeds/conversion/writer/writer.go b/vulnfeeds/conversion/writer/writer.go index f30fed98e7f..ce5826d66c6 100644 --- a/vulnfeeds/conversion/writer/writer.go +++ b/vulnfeeds/conversion/writer/writer.go @@ -256,7 +256,11 @@ func UploadVulnsToGCS( } if doDeletions { - HandleDeletion(ctx, outBkt, osvOutputPath, vulnerabilities) + validIDs := make([]string, len(vulnerabilities)) + for i, v := range vulnerabilities { + validIDs[i] = v.GetId() + } + HandleDeletion(ctx, outBkt, osvOutputPath, validIDs) } gcsHelper, err = gcs.InitUploadPool(ctx, numWorkers, outputBucketName) @@ -292,7 +296,7 @@ func UploadVulnsToGCS( } } -func HandleDeletion(ctx context.Context, outBkt *storage.BucketHandle, osvOutputPath string, vulnerabilities []*osvschema.Vulnerability) { +func HandleDeletion(ctx context.Context, outBkt *storage.BucketHandle, osvOutputPath string, validVulnIDs []string) { // Check if any need to be deleted bucketObjects, err := gcs.ListBucketObjects(ctx, outBkt, osvOutputPath) if err != nil { @@ -300,8 +304,8 @@ func HandleDeletion(ctx context.Context, outBkt *storage.BucketHandle, osvOutput return } vulnFilenames := make(map[string]bool) - for _, v := range vulnerabilities { - filename := v.GetId() + ".json" + for _, id := range validVulnIDs { + filename := id + ".json" filePath := path.Join(osvOutputPath, filename) vulnFilenames[filePath] = true } diff --git a/vulnfeeds/conversion/writer/writer_test.go b/vulnfeeds/conversion/writer/writer_test.go index 72fb09c4ca1..2eb29d2aa4e 100644 --- a/vulnfeeds/conversion/writer/writer_test.go +++ b/vulnfeeds/conversion/writer/writer_test.go @@ -334,12 +334,9 @@ func TestHandleDeletion(t *testing.T) { } w2.Close() - vulnerabilities := []*osvschema.Vulnerability{ - {Id: "CVE-2023-1111"}, - {Id: "CVE-2023-3333"}, - } + validIDs := []string{"CVE-2023-1111", "CVE-2023-3333"} - HandleDeletion(ctx, bkt, "", vulnerabilities) + HandleDeletion(ctx, bkt, "", validIDs) // CVE-2023-1111.json should still exist if _, err := bkt.Object("CVE-2023-1111.json").Attrs(ctx); err != nil { From 2a4867d38caddda1d48767425409e5fabf20c154 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 9 Jul 2026 05:17:36 +0000 Subject: [PATCH 03/13] improve naming --- vulnfeeds/cmd/combine-to-osv/main.go | 4 +- vulnfeeds/cmd/combine-to-osv/main_test.go | 45 ++++++++--------------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index ba59d4b3184..34bf25bffcf 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -121,7 +121,7 @@ func readVulnerability(ctx context.Context, client *storage.Client, fullPath str return &vuln, nil } -func combineOneOSVRecord(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability, mandatoryCVEIDs []string) *osvschema.Vulnerability { +func combineIntoOSV(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability, mandatoryCVEIDs []string) *osvschema.Vulnerability { var baseOSV *osvschema.Vulnerability if cve5 != nil && nvd != nil { baseOSV = combineTwoOSVRecords(cve5, nvd) @@ -172,7 +172,7 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan readWg.Wait() - combined := combineOneOSVRecord(work.ID, cve5, nvd, mandatoryCVEIDs) + combined := combineIntoOSV(work.ID, cve5, nvd, mandatoryCVEIDs) if combined != nil { vulnChan <- combined } diff --git a/vulnfeeds/cmd/combine-to-osv/main_test.go b/vulnfeeds/cmd/combine-to-osv/main_test.go index 0a1e7f5e1c5..bf82f402727 100644 --- a/vulnfeeds/cmd/combine-to-osv/main_test.go +++ b/vulnfeeds/cmd/combine-to-osv/main_test.go @@ -5,7 +5,6 @@ import ( "log/slog" "os" "path/filepath" - "slices" "sort" "strings" "testing" @@ -61,38 +60,26 @@ func loadOSV(osvPath string) map[models.CVEID]*osvschema.Vulnerability { return allVulns } -// combineIntoOSV creates OSV entry by combining loaded CVEs from NVD and PackageInfo information from security advisories. -func combineIntoOSV(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability, mandatoryCVEIDs []string) map[models.CVEID]*osvschema.Vulnerability { +// combinePrep creates OSV entry by combining loaded CVEs from NVD and PackageInfo information from security advisories. +func combinePrep(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability, mandatoryCVEIDs []string) map[models.CVEID]*osvschema.Vulnerability { osvRecords := make(map[models.CVEID]*osvschema.Vulnerability) - // Iterate through CVEs from security advisories (cve5) as the base - for cveID, cve5 := range cve5osv { - var baseOSV *osvschema.Vulnerability - nvd, ok := nvdosv[cveID] - - if ok { - baseOSV = combineTwoOSVRecords(cve5, nvd) - // The CVE is processed, so remove it from the nvdosv map to avoid re-processing. - delete(nvdosv, cveID) - } else { - baseOSV = cve5 - } - - if len(baseOSV.GetAffected()) == 0 || !hasRanges(baseOSV.GetAffected()) { - // check if part exists. - if !slices.Contains(mandatoryCVEIDs, string(cveID)) { - continue - } - } - osvRecords[cveID] = baseOSV + // Collect all unique CVE IDs + allIDs := make(map[models.CVEID]bool) + for id := range cve5osv { + allIDs[id] = true + } + for id := range nvdosv { + allIDs[id] = true } - // Add any remaining CVEs from NVD that were not in the advisory data. - for cveID, nvd := range nvdosv { - if len(nvd.GetAffected()) == 0 || !hasRanges(nvd.GetAffected()) { - continue + for id := range allIDs { + cve5 := cve5osv[id] + nvd := nvdosv[id] + combined := combineIntoOSV(id, cve5, nvd, mandatoryCVEIDs) + if combined != nil { + osvRecords[id] = combined } - osvRecords[cveID] = nvd } return osvRecords @@ -110,7 +97,7 @@ func TestCombineIntoOSV(t *testing.T) { } noPkgCVEs := []string{"CVE-2023-0003"} - combined := combineIntoOSV(cve5osv, nvdosvCopy, noPkgCVEs) + combined := combinePrep(cve5osv, nvdosvCopy, noPkgCVEs) // Expected results // CVE-2023-1234: merged From 89df1d0c6145bb5ec741404501e920fcca050072 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 9 Jul 2026 06:04:22 +0000 Subject: [PATCH 04/13] fix englang bug --- vulnfeeds/cmd/combine-to-osv/main.go | 6 ++++++ vulnfeeds/models/cve.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index 34bf25bffcf..22352e3b446 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -420,6 +420,12 @@ func listBucketObjects(ctx context.Context, client *storage.Client, bucketName s // combineTwoOSVRecords takes two osv records and combines them into one func combineTwoOSVRecords(cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { baseOSV := cve5 + if baseOSV.GetDetails() == "" && nvd.GetDetails() != "" { + baseOSV.Details = nvd.Details + } + if baseOSV.GetSummary() == "" && nvd.GetSummary() != "" { + baseOSV.Summary = nvd.Summary + } combinedAffected := pickAffectedInformation(cve5.GetAffected(), nvd.GetAffected()) baseOSV.Affected = combinedAffected diff --git a/vulnfeeds/models/cve.go b/vulnfeeds/models/cve.go index 875896834dd..f2393c004a1 100644 --- a/vulnfeeds/models/cve.go +++ b/vulnfeeds/models/cve.go @@ -146,7 +146,7 @@ type CVE5 struct { func EnglishDescription(descriptions []LangString) string { for _, desc := range descriptions { - if desc.Lang == "en" { + if desc.Lang == "en" || strings.HasPrefix(desc.Lang, "en-") { return desc.Value } } From bdbce55647e310cca02195bd34593f6bb0ba80e5 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Fri, 10 Jul 2026 00:43:41 +0000 Subject: [PATCH 05/13] address some comments, remove --- vulnfeeds/cmd/combine-to-osv/main.go | 78 +++++++++------------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index 22352e3b446..983bbaced47 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -11,6 +11,7 @@ import ( "io/fs" "log/slog" "os" + "path" "path/filepath" "slices" "strings" @@ -44,7 +45,12 @@ type CVEWorkItem struct { } func cveIDFromPath(p string) models.CVEID { - base := filepath.Base(p) + var base string + if strings.HasPrefix(p, "gs://") { + base = path.Base(p) + } else { + base = filepath.Base(p) + } id := strings.TrimSuffix(base, ".json") if strings.HasPrefix(id, "CVE-") { return models.CVEID(id) @@ -121,7 +127,7 @@ func readVulnerability(ctx context.Context, client *storage.Client, fullPath str return &vuln, nil } -func combineIntoOSV(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability, mandatoryCVEIDs []string) *osvschema.Vulnerability { +func combineIntoOSV(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { var baseOSV *osvschema.Vulnerability if cve5 != nil && nvd != nil { baseOSV = combineTwoOSVRecords(cve5, nvd) @@ -133,16 +139,10 @@ func combineIntoOSV(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvs return nil } - if len(baseOSV.GetAffected()) == 0 || !hasRanges(baseOSV.GetAffected()) { - if !slices.Contains(mandatoryCVEIDs, string(cveID)) { - return nil - } - } - return baseOSV } -func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan <-chan *CVEWorkItem, vulnChan chan<- *osvschema.Vulnerability, mandatoryCVEIDs []string) { +func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan <-chan *CVEWorkItem, vulnChan chan<- *osvschema.Vulnerability) { for work := range workChan { var cve5, nvd *osvschema.Vulnerability var cve5Err, nvdErr error @@ -154,7 +154,7 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan defer readWg.Done() cve5, cve5Err = readVulnerability(ctx, client, work.CVE5Path) if cve5Err != nil { - logger.Warn("Failed to read CVE5", slog.String("id", string(work.ID)), slog.Any("err", cve5Err)) + logger.Error("Failed to read CVE5", slog.String("id", string(work.ID)), slog.Any("err", cve5Err)) } }() } @@ -165,14 +165,18 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan defer readWg.Done() nvd, nvdErr = readVulnerability(ctx, client, work.NVDPath) if nvdErr != nil { - logger.Warn("Failed to read NVD", slog.String("id", string(work.ID)), slog.Any("err", nvdErr)) + logger.Error("Failed to read NVD", slog.String("id", string(work.ID)), slog.Any("err", nvdErr)) } }() } readWg.Wait() - combined := combineIntoOSV(work.ID, cve5, nvd, mandatoryCVEIDs) + if cve5Err != nil || nvdErr != nil { + continue + } + + combined := combineIntoOSV(work.ID, cve5, nvd) if combined != nil { vulnChan <- combined } @@ -205,49 +209,10 @@ func main() { } defer client.Close() - var debianCVEs, alpineCVEs []string - var debianErr, alpineErr error - - var wg sync.WaitGroup - wg.Add(2) - - go func() { - defer wg.Done() - debianCVEs, debianErr = listBucketObjects(ctx, client, "osv-test-debian-osv", "/debian-cve-osv") - if debianErr != nil { - logger.Warn("Failed to list debian cves", slog.Any("err", debianErr)) - } else { - for i, filename := range debianCVEs { - cve := extractCVEName(filename, "DEBIAN-") - if cve != "" { - debianCVEs[i] = cve - } - } - } - }() - - go func() { - defer wg.Done() - alpineCVEs, alpineErr = listBucketObjects(ctx, client, "osv-test-cve-osv-conversion", "/alpine") - if alpineErr != nil { - logger.Warn("Failed to list alpine cves", slog.Any("err", alpineErr)) - } else { - for i, filename := range alpineCVEs { - cve := extractCVEName(filename, "ALPINE-") - if cve != "" { - alpineCVEs[i] = cve - } - } - } - }() - - wg.Wait() - - mandatoryCVEIDs := append(debianCVEs, alpineCVEs...) //nolint:gocritic - // List CVE5 and NVD objects var cve5Files, nvdFiles []string var cve5ListErr, nvdListErr error + logger.Info("Starting to list CVE5 and NVD objects") var listWg sync.WaitGroup listWg.Add(2) @@ -272,6 +237,7 @@ func main() { listWg.Wait() // Build work items + logger.Info("Starting to build work items") workItems := make(map[models.CVEID]*CVEWorkItem) for _, f := range cve5Files { id := cveIDFromPath(f) @@ -311,6 +277,12 @@ func main() { } // Start Channels + // Channel Flow: + // + // [workItems] ---workChan---> [readAndCombineWorker] ---vulnChan---> [Collector] ---validVulnChan---> [VulnWorker] ---> [GCS/Disk] + // | + // +---> (filters invalid) + logger.Info("Starting processing channels and workers") vulnChan := make(chan *osvschema.Vulnerability, *numWorkers) validVulnChan := make(chan *osvschema.Vulnerability, *numWorkers) workChan := make(chan *CVEWorkItem, *numWorkers) @@ -353,7 +325,7 @@ func main() { readWg.Add(1) go func() { defer readWg.Done() - readAndCombineWorker(ctx, client, workChan, vulnChan, mandatoryCVEIDs) + readAndCombineWorker(ctx, client, workChan, vulnChan) }() } From b9b04b993fe9c8b0d2e4646ca9786ff98fc5f01c Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Fri, 10 Jul 2026 06:56:23 +0000 Subject: [PATCH 06/13] renaming stuff --- vulnfeeds/cmd/combine-to-osv/main.go | 50 +++++++++++------------ vulnfeeds/cmd/combine-to-osv/main_test.go | 36 ++++++++-------- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index 983bbaced47..6703bd831c2 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -146,12 +146,12 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan for work := range workChan { var cve5, nvd *osvschema.Vulnerability var cve5Err, nvdErr error - var readWg sync.WaitGroup + var readVulnsWg sync.WaitGroup if work.CVE5Path != "" { - readWg.Add(1) + readVulnsWg.Add(1) go func() { - defer readWg.Done() + defer readVulnsWg.Done() cve5, cve5Err = readVulnerability(ctx, client, work.CVE5Path) if cve5Err != nil { logger.Error("Failed to read CVE5", slog.String("id", string(work.ID)), slog.Any("err", cve5Err)) @@ -160,9 +160,9 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan } if work.NVDPath != "" { - readWg.Add(1) + readVulnsWg.Add(1) go func() { - defer readWg.Done() + defer readVulnsWg.Done() nvd, nvdErr = readVulnerability(ctx, client, work.NVDPath) if nvdErr != nil { logger.Error("Failed to read NVD", slog.String("id", string(work.ID)), slog.Any("err", nvdErr)) @@ -170,7 +170,7 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan }() } - readWg.Wait() + readVulnsWg.Wait() if cve5Err != nil || nvdErr != nil { continue @@ -213,11 +213,11 @@ func main() { var cve5Files, nvdFiles []string var cve5ListErr, nvdListErr error logger.Info("Starting to list CVE5 and NVD objects") - var listWg sync.WaitGroup - listWg.Add(2) + var listObjWg sync.WaitGroup + listObjWg.Add(2) go func() { - defer listWg.Done() + defer listObjWg.Done() logger.Info("Listing CVE5 objects", slog.String("path", *cve5Path)) cve5Files, cve5ListErr = listObjects(ctx, client, *cve5Path) if cve5ListErr != nil { @@ -226,7 +226,7 @@ func main() { }() go func() { - defer listWg.Done() + defer listObjWg.Done() logger.Info("Listing NVD objects", slog.String("path", *nvdPath)) nvdFiles, nvdListErr = listObjects(ctx, client, *nvdPath) if nvdListErr != nil { @@ -234,7 +234,7 @@ func main() { } }() - listWg.Wait() + listObjWg.Wait() // Build work items logger.Info("Starting to build work items") @@ -288,23 +288,23 @@ func main() { workChan := make(chan *CVEWorkItem, *numWorkers) // Start VulnWorkers (Upload side) - var uploadWg sync.WaitGroup + var uploadVulnsWg sync.WaitGroup var successCount atomic.Uint64 for range *numWorkers { - uploadWg.Add(1) + uploadVulnsWg.Add(1) go func() { - defer uploadWg.Done() + defer uploadVulnsWg.Done() writer.VulnWorker(ctx, validVulnChan, outBkt, overridesBkt, gcsHelper, *osvOutputPath, &successCount) }() } // Interpose Collector to gather valid IDs var validIDs []string - var idWg sync.WaitGroup - idWg.Add(1) + var collectValidIDsWg sync.WaitGroup + collectValidIDsWg.Add(1) totalWork := len(workItems) go func() { - defer idWg.Done() + defer collectValidIDsWg.Done() count := 0 for v := range vulnChan { count++ @@ -320,11 +320,11 @@ func main() { }() // Start ReadAndCombineWorkers (Read side) - var readWg sync.WaitGroup + var readAndCombineWg sync.WaitGroup for range *numWorkers { - readWg.Add(1) + readAndCombineWg.Add(1) go func() { - defer readWg.Done() + defer readAndCombineWg.Done() readAndCombineWorker(ctx, client, workChan, vulnChan) }() } @@ -338,12 +338,12 @@ func main() { }() // Wait for reads to finish - readWg.Wait() + readAndCombineWg.Wait() close(vulnChan) // Wait for collector and uploads to finish - idWg.Wait() - uploadWg.Wait() + collectValidIDsWg.Wait() + uploadVulnsWg.Wait() logger.Info("Successfully processed OSV files", slog.Int("count", len(validIDs))) if outBkt == nil && gcsHelper == nil { @@ -393,10 +393,10 @@ func listBucketObjects(ctx context.Context, client *storage.Client, bucketName s func combineTwoOSVRecords(cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { baseOSV := cve5 if baseOSV.GetDetails() == "" && nvd.GetDetails() != "" { - baseOSV.Details = nvd.Details + baseOSV.Details = nvd.GetDetails() } if baseOSV.GetSummary() == "" && nvd.GetSummary() != "" { - baseOSV.Summary = nvd.Summary + baseOSV.Summary = nvd.GetSummary() } combinedAffected := pickAffectedInformation(cve5.GetAffected(), nvd.GetAffected()) diff --git a/vulnfeeds/cmd/combine-to-osv/main_test.go b/vulnfeeds/cmd/combine-to-osv/main_test.go index bf82f402727..1217a6afef8 100644 --- a/vulnfeeds/cmd/combine-to-osv/main_test.go +++ b/vulnfeeds/cmd/combine-to-osv/main_test.go @@ -61,7 +61,7 @@ func loadOSV(osvPath string) map[models.CVEID]*osvschema.Vulnerability { } // combinePrep creates OSV entry by combining loaded CVEs from NVD and PackageInfo information from security advisories. -func combinePrep(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability, mandatoryCVEIDs []string) map[models.CVEID]*osvschema.Vulnerability { +func combinePrep(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability) map[models.CVEID]*osvschema.Vulnerability { osvRecords := make(map[models.CVEID]*osvschema.Vulnerability) // Collect all unique CVE IDs @@ -76,7 +76,7 @@ func combinePrep(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[m for id := range allIDs { cve5 := cve5osv[id] nvd := nvdosv[id] - combined := combineIntoOSV(id, cve5, nvd, mandatoryCVEIDs) + combined := combineIntoOSV(id, cve5, nvd) if combined != nil { osvRecords[id] = combined } @@ -95,18 +95,16 @@ func TestCombineIntoOSV(t *testing.T) { for k, v := range nvdosv { nvdosvCopy[k] = v } - noPkgCVEs := []string{"CVE-2023-0003"} - - combined := combinePrep(cve5osv, nvdosvCopy, noPkgCVEs) + combined := combinePrep(cve5osv, nvdosvCopy) // Expected results // CVE-2023-1234: merged // CVE-2023-0001: from cve5 only // CVE-2023-0002: from nvd only - // CVE-2023-0003: from cve5, no affected, but in noPkgCVEs - // CVE-2023-0004: from cve5, no affected, not in noPkgCVEs, so skipped - if len(combined) != 2 { - t.Errorf("Expected 2 combined vulnerabilities, got %d", len(combined)) + // CVE-2023-0003: from cve5 only + // CVE-2023-0004: from cve5 only + if len(combined) != 5 { + t.Errorf("Expected 5 combined vulnerabilities, got %d", len(combined)) } // Test case 1: Merged CVE @@ -165,24 +163,24 @@ func TestCombineIntoOSV(t *testing.T) { t.Errorf("CVE-2023-1234: affected range mismatch (-want +got):\n%s", diff) } - // Test case 2: CVE only in cve5 (has no ranges, so it should be skipped) - if _, ok = combined["CVE-2023-0001"]; ok { - t.Error("Expected combined map to NOT contain CVE-2023-0001 because it has no ranges") + // Test case 2: CVE only in cve5 (has no ranges, but should be kept) + if _, ok = combined["CVE-2023-0001"]; !ok { + t.Error("Expected combined map to contain CVE-2023-0001") } - // Test case 3: CVE only in nvd (has no ranges, so it should be skipped) - if _, ok = combined["CVE-2023-0002"]; ok { - t.Error("Expected combined map to NOT contain CVE-2023-0002 because it has no ranges") + // Test case 3: CVE only in nvd (has no ranges, but should be kept) + if _, ok = combined["CVE-2023-0002"]; !ok { + t.Error("Expected combined map to contain CVE-2023-0002") } - // Test case 4: No ranges, in noPkgCVEs (should be kept) + // Test case 4: No ranges, no affected (should be kept) if _, ok = combined["CVE-2023-0003"]; !ok { t.Error("Expected combined map to contain CVE-2023-0003") } - // Test case 5: No ranges, not in noPkgCVEs (should be skipped) - if _, ok = combined["CVE-2023-0004"]; ok { - t.Error("Expected combined map to NOT contain CVE-2023-0004") + // Test case 5: No ranges, no affected (should be kept) + if _, ok = combined["CVE-2023-0004"]; !ok { + t.Error("Expected combined map to contain CVE-2023-0004") } } From 1a5bb0b44f44c75dc5a5ca5eb2e8ff796ba43cca Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Fri, 10 Jul 2026 07:02:45 +0000 Subject: [PATCH 07/13] fix linting issues --- vulnfeeds/cmd/combine-to-osv/main.go | 49 +---------------------- vulnfeeds/cmd/combine-to-osv/main_test.go | 2 +- 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index 6703bd831c2..c5acfdf83ca 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -4,7 +4,6 @@ package main import ( "cmp" "context" - "errors" "flag" "fmt" "io" @@ -26,7 +25,6 @@ import ( "github.com/google/osv/vulnfeeds/utility" "github.com/google/osv/vulnfeeds/utility/logger" "github.com/ossf/osv-schema/bindings/go/osvschema" - "google.golang.org/api/iterator" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) @@ -127,7 +125,7 @@ func readVulnerability(ctx context.Context, client *storage.Client, fullPath str return &vuln, nil } -func combineIntoOSV(cveID models.CVEID, cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { +func combineIntoOSV(cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { var baseOSV *osvschema.Vulnerability if cve5 != nil && nvd != nil { baseOSV = combineTwoOSVRecords(cve5, nvd) @@ -176,7 +174,7 @@ func readAndCombineWorker(ctx context.Context, client *storage.Client, workChan continue } - combined := combineIntoOSV(work.ID, cve5, nvd) + combined := combineIntoOSV(cve5, nvd) if combined != nil { vulnChan <- combined } @@ -356,39 +354,6 @@ func main() { } } -// extractCVEName extracts the CVE name from a given filename and prefix. -// It returns an empty string if the filename does not start with "CVE". -func extractCVEName(filename string, prefix string) string { - cleaned := strings.TrimPrefix(filename, prefix) - cleaned = strings.TrimSuffix(cleaned, ".json") - pre := strings.Split(cleaned, "-") - if pre[0] != "CVE" { - return "" - } - - return cleaned -} - -// listBucketObjects lists the names of all objects in a Google Cloud Storage bucket. -// It does not download the file contents. -func listBucketObjects(ctx context.Context, client *storage.Client, bucketName string, prefix string) ([]string, error) { - bucket := client.Bucket(bucketName) - it := bucket.Objects(ctx, &storage.Query{Prefix: prefix}) - var filenames []string - for { - attrs, err := it.Next() - if errors.Is(err, iterator.Done) { - break // All objects have been listed. - } - if err != nil { - return nil, fmt.Errorf("bucket.Objects: %w", err) - } - filenames = append(filenames, attrs.Name) - } - - return filenames, nil -} - // combineTwoOSVRecords takes two osv records and combines them into one func combineTwoOSVRecords(cve5 *osvschema.Vulnerability, nvd *osvschema.Vulnerability) *osvschema.Vulnerability { baseOSV := cve5 @@ -1058,16 +1023,6 @@ func pickBestRange(cve5Range *osvschema.Range, nvdRange *osvschema.Range) *osvsc return merged } -func hasRanges(affected []*osvschema.Affected) bool { - for _, a := range affected { - if len(a.GetRanges()) > 0 { - return true - } - } - - return false -} - // getRangeBoundaryVersions extracts the introduced, last_affected and fixed versions from a slice of OSV events. // It iterates through the events and returns the last non-empty "introduced", "last_affected" and "fixed" versions found. func getRangeBoundaryVersions(events []*osvschema.Event) (introduced, lastAffected, fixed string) { diff --git a/vulnfeeds/cmd/combine-to-osv/main_test.go b/vulnfeeds/cmd/combine-to-osv/main_test.go index 1217a6afef8..1b1414a7d72 100644 --- a/vulnfeeds/cmd/combine-to-osv/main_test.go +++ b/vulnfeeds/cmd/combine-to-osv/main_test.go @@ -76,7 +76,7 @@ func combinePrep(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[m for id := range allIDs { cve5 := cve5osv[id] nvd := nvdosv[id] - combined := combineIntoOSV(id, cve5, nvd) + combined := combineIntoOSV(cve5, nvd) if combined != nil { osvRecords[id] = combined } From 5bb9d742ab960b5d7dc5b08285a19e0f5574dfa6 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 13 Jul 2026 00:43:34 +0000 Subject: [PATCH 08/13] add input bucket --- .../gke-workers/environments/oss-vdb/combine-to-osv.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml b/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml index 59604ef3283..3be27118a36 100644 --- a/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml +++ b/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml @@ -15,3 +15,5 @@ spec: value: oss-vdb - name: OUTPUT_GCS_BUCKET value: cve-osv-conversion + - name: INPUT_GCS_BUCKET + value: cve-osv-conversion \ No newline at end of file From 4b719b4532f71bd980712ef02048b8045631f144 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 13 Jul 2026 00:44:13 +0000 Subject: [PATCH 09/13] revert default --- vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh b/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh index 908547c1216..4a0a4594a0d 100755 --- a/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh +++ b/vulnfeeds/cmd/combine-to-osv/run_combine_to_osv_convert.sh @@ -16,8 +16,8 @@ trap '__error_handing__ $? $LINENO' ERR set -eu -INPUT_BUCKET="${INPUT_GCS_BUCKET:=osv-test-cve-osv-conversion}" -OUTPUT_BUCKET="${OUTPUT_GCS_BUCKET:=osv-test-cve-osv-conversion}" +INPUT_BUCKET="${INPUT_GCS_BUCKET:=cve-osv-conversion}" +OUTPUT_BUCKET="${OUTPUT_GCS_BUCKET:=cve-osv-conversion}" NUM_WORKERS="${NUM_WORKERS:=64}" OSV_OUTPUT="osv-output" From a8df5d104c6a43721b72812a82f52b5ee913fc8d Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 13 Jul 2026 00:47:29 +0000 Subject: [PATCH 10/13] more workers --- .../gke-workers/environments/oss-vdb-test/combine-to-osv.yaml | 2 ++ .../gke-workers/environments/oss-vdb/combine-to-osv.yaml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/deployment/clouddeploy/gke-workers/environments/oss-vdb-test/combine-to-osv.yaml b/deployment/clouddeploy/gke-workers/environments/oss-vdb-test/combine-to-osv.yaml index 6ab13e680c4..124751dd550 100644 --- a/deployment/clouddeploy/gke-workers/environments/oss-vdb-test/combine-to-osv.yaml +++ b/deployment/clouddeploy/gke-workers/environments/oss-vdb-test/combine-to-osv.yaml @@ -16,3 +16,5 @@ spec: value: osv-test-cve-osv-conversion - name: OUTPUT_GCS_BUCKET value: osv-test-cve-osv-conversion + - name: NUM_WORKERS + value: "1000" diff --git a/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml b/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml index 3be27118a36..15be287c44d 100644 --- a/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml +++ b/deployment/clouddeploy/gke-workers/environments/oss-vdb/combine-to-osv.yaml @@ -16,4 +16,6 @@ spec: - name: OUTPUT_GCS_BUCKET value: cve-osv-conversion - name: INPUT_GCS_BUCKET - value: cve-osv-conversion \ No newline at end of file + value: cve-osv-conversion + - name: NUM_WORKERS + value: "1000" \ No newline at end of file From af7236153c1c527e2d9d96b8e247ad3c82ca939b Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 13 Jul 2026 03:47:50 +0000 Subject: [PATCH 11/13] remove test that wasn't really testing anything anymore --- vulnfeeds/cmd/combine-to-osv/main_test.go | 169 ---------------------- 1 file changed, 169 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main_test.go b/vulnfeeds/cmd/combine-to-osv/main_test.go index 1b1414a7d72..1f784cdc316 100644 --- a/vulnfeeds/cmd/combine-to-osv/main_test.go +++ b/vulnfeeds/cmd/combine-to-osv/main_test.go @@ -1,21 +1,13 @@ package main import ( - "io/fs" - "log/slog" - "os" - "path/filepath" "sort" - "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/utility/logger" "github.com/ossf/osv-schema/bindings/go/osvschema" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -23,167 +15,6 @@ import ( const testdataPath = "../../test_data/combine-to-osv" -// loadOSV loads OSV vulnerabilities from a given local directory path. -// It returns a map of CVE IDs to their corresponding Vulnerability objects. -func loadOSV(osvPath string) map[models.CVEID]*osvschema.Vulnerability { - allVulns := make(map[models.CVEID]*osvschema.Vulnerability) - logger.Info("Loading OSV records from local path", slog.String("path", osvPath)) - err := filepath.WalkDir(osvPath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() || !strings.HasSuffix(path, ".json") || strings.HasSuffix(path, ".metrics.json") { - return nil - } - - file, err := os.ReadFile(path) - if err != nil { - logger.Warn("Failed to open OSV JSON file", slog.String("path", path), slog.Any("err", err)) - return nil - } - - var vuln osvschema.Vulnerability - decodeErr := protojson.Unmarshal(file, &vuln) - if decodeErr != nil { - logger.Error("Failed to decode, skipping", slog.String("file", path), slog.Any("err", decodeErr)) - return nil - } - allVulns[models.CVEID(vuln.GetId())] = &vuln - - return nil - }) - - if err != nil { - logger.Fatal("Failed to walk OSV directory", slog.String("path", osvPath), slog.Any("err", err)) - } - - return allVulns -} - -// combinePrep creates OSV entry by combining loaded CVEs from NVD and PackageInfo information from security advisories. -func combinePrep(cve5osv map[models.CVEID]*osvschema.Vulnerability, nvdosv map[models.CVEID]*osvschema.Vulnerability) map[models.CVEID]*osvschema.Vulnerability { - osvRecords := make(map[models.CVEID]*osvschema.Vulnerability) - - // Collect all unique CVE IDs - allIDs := make(map[models.CVEID]bool) - for id := range cve5osv { - allIDs[id] = true - } - for id := range nvdosv { - allIDs[id] = true - } - - for id := range allIDs { - cve5 := cve5osv[id] - nvd := nvdosv[id] - combined := combineIntoOSV(cve5, nvd) - if combined != nil { - osvRecords[id] = combined - } - } - - return osvRecords -} - -func TestCombineIntoOSV(t *testing.T) { - cve5Path := filepath.Join(testdataPath, "cve5") - nvdPath := filepath.Join(testdataPath, "nvd") - - cve5osv := loadOSV(cve5Path) - nvdosv := loadOSV(nvdPath) - nvdosvCopy := make(map[models.CVEID]*osvschema.Vulnerability) - for k, v := range nvdosv { - nvdosvCopy[k] = v - } - combined := combinePrep(cve5osv, nvdosvCopy) - - // Expected results - // CVE-2023-1234: merged - // CVE-2023-0001: from cve5 only - // CVE-2023-0002: from nvd only - // CVE-2023-0003: from cve5 only - // CVE-2023-0004: from cve5 only - if len(combined) != 5 { - t.Errorf("Expected 5 combined vulnerabilities, got %d", len(combined)) - } - - // Test case 1: Merged CVE - cve1234, ok := combined["CVE-2023-1234"] - if !ok { - t.Fatal("Expected combined map to contain CVE-2023-1234") - } - - // Check modified and published dates - expectedModified, _ := time.Parse(time.RFC3339, "2023-01-02T12:00:00Z") - if !cve1234.GetModified().AsTime().Equal(expectedModified) { - t.Errorf("CVE-2023-1234: expected modified time %v, got %v", expectedModified, cve1234.GetModified()) - } - expectedPublished, _ := time.Parse(time.RFC3339, "2023-01-01T09:00:00Z") - if !cve1234.GetPublished().AsTime().Equal(expectedPublished) { - t.Errorf("CVE-2023-1234: expected published time %v, got %v", expectedPublished, cve1234.GetPublished()) - } - - // Check references - if len(cve1234.GetReferences()) != 2 { - t.Errorf("CVE-2023-1234: expected 2 references, got %d", len(cve1234.GetReferences())) - } - - // Check aliases - if len(cve1234.GetAliases()) != 2 { - t.Errorf("CVE-2023-1234: expected 2 aliases, got %d", len(cve1234.GetAliases())) - } - - // Check affected (based on pickAffectedInformation logic) - var affectedForRepoA *osvschema.Affected - foundAffected := false - for _, a := range cve1234.GetAffected() { - if len(a.GetRanges()) > 0 && a.GetRanges()[0].GetRepo() == "https://example.com/repo/a" { - affectedForRepoA = a - foundAffected = true - - break - } - } - if !foundAffected { - t.Fatal("Did not find affected for repo https://example.com/repo/a") - } - - expectedRange := &osvschema.Range{ - Type: osvschema.Range_GIT, - Repo: "https://example.com/repo/a", - Events: []*osvschema.Event{ - {Introduced: "1.0.0"}, - {Fixed: "1.0.1"}, - }, - } - - // The current logic for pickAffectedInformation when len(cveRanges) == 1 && len(nvdRanges) == 1 - // is to prefer cve5 data. - if diff := cmp.Diff(expectedRange, affectedForRepoA.GetRanges()[0], protocmp.Transform()); diff != "" { - t.Errorf("CVE-2023-1234: affected range mismatch (-want +got):\n%s", diff) - } - - // Test case 2: CVE only in cve5 (has no ranges, but should be kept) - if _, ok = combined["CVE-2023-0001"]; !ok { - t.Error("Expected combined map to contain CVE-2023-0001") - } - - // Test case 3: CVE only in nvd (has no ranges, but should be kept) - if _, ok = combined["CVE-2023-0002"]; !ok { - t.Error("Expected combined map to contain CVE-2023-0002") - } - - // Test case 4: No ranges, no affected (should be kept) - if _, ok = combined["CVE-2023-0003"]; !ok { - t.Error("Expected combined map to contain CVE-2023-0003") - } - - // Test case 5: No ranges, no affected (should be kept) - if _, ok = combined["CVE-2023-0004"]; !ok { - t.Error("Expected combined map to contain CVE-2023-0004") - } -} - func TestPickAffectedInformation(t *testing.T) { repoA := "https://example.com/repo/a" repoB := "https://example.com/repo/b" From 16f0efab12a7ce9f0ed659230a9d673789f1d9a2 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 13 Jul 2026 04:05:42 +0000 Subject: [PATCH 12/13] rename validVulnChan --- vulnfeeds/cmd/combine-to-osv/main.go | 10 +++++----- vulnfeeds/cmd/combine-to-osv/main_test.go | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index c5acfdf83ca..3faaef3299f 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -277,12 +277,12 @@ func main() { // Start Channels // Channel Flow: // - // [workItems] ---workChan---> [readAndCombineWorker] ---vulnChan---> [Collector] ---validVulnChan---> [VulnWorker] ---> [GCS/Disk] + // [workItems] ---workChan---> [readAndCombineWorker] ---vulnChan---> [Collector] ---outputVulnChan---> [VulnWorker] ---> [GCS/Disk] // | // +---> (filters invalid) logger.Info("Starting processing channels and workers") vulnChan := make(chan *osvschema.Vulnerability, *numWorkers) - validVulnChan := make(chan *osvschema.Vulnerability, *numWorkers) + outputVulnChan := make(chan *osvschema.Vulnerability, *numWorkers) workChan := make(chan *CVEWorkItem, *numWorkers) // Start VulnWorkers (Upload side) @@ -292,7 +292,7 @@ func main() { uploadVulnsWg.Add(1) go func() { defer uploadVulnsWg.Done() - writer.VulnWorker(ctx, validVulnChan, outBkt, overridesBkt, gcsHelper, *osvOutputPath, &successCount) + writer.VulnWorker(ctx, outputVulnChan, outBkt, overridesBkt, gcsHelper, *osvOutputPath, &successCount) }() } @@ -309,12 +309,12 @@ func main() { if len(v.GetAffected()) > 0 { validIDs = append(validIDs, v.GetId()) } - validVulnChan <- v + outputVulnChan <- v if count%1000 == 0 { logger.Info("Processed CVEs", slog.Int("count", count), slog.Int("total", totalWork), slog.Int("percent", (count*100)/totalWork)) } } - close(validVulnChan) + close(outputVulnChan) }() // Start ReadAndCombineWorkers (Read side) diff --git a/vulnfeeds/cmd/combine-to-osv/main_test.go b/vulnfeeds/cmd/combine-to-osv/main_test.go index 1f784cdc316..90f0e9873b8 100644 --- a/vulnfeeds/cmd/combine-to-osv/main_test.go +++ b/vulnfeeds/cmd/combine-to-osv/main_test.go @@ -13,8 +13,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -const testdataPath = "../../test_data/combine-to-osv" - func TestPickAffectedInformation(t *testing.T) { repoA := "https://example.com/repo/a" repoB := "https://example.com/repo/b" From 7b8fcd7e4896e893dfe7a22656209cad303251e6 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 13 Jul 2026 04:06:35 +0000 Subject: [PATCH 13/13] rename again --- vulnfeeds/cmd/combine-to-osv/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vulnfeeds/cmd/combine-to-osv/main.go b/vulnfeeds/cmd/combine-to-osv/main.go index 3faaef3299f..d8dfb595b48 100644 --- a/vulnfeeds/cmd/combine-to-osv/main.go +++ b/vulnfeeds/cmd/combine-to-osv/main.go @@ -282,7 +282,7 @@ func main() { // +---> (filters invalid) logger.Info("Starting processing channels and workers") vulnChan := make(chan *osvschema.Vulnerability, *numWorkers) - outputVulnChan := make(chan *osvschema.Vulnerability, *numWorkers) + uploadVulnsChan := make(chan *osvschema.Vulnerability, *numWorkers) workChan := make(chan *CVEWorkItem, *numWorkers) // Start VulnWorkers (Upload side) @@ -292,7 +292,7 @@ func main() { uploadVulnsWg.Add(1) go func() { defer uploadVulnsWg.Done() - writer.VulnWorker(ctx, outputVulnChan, outBkt, overridesBkt, gcsHelper, *osvOutputPath, &successCount) + writer.VulnWorker(ctx, uploadVulnsChan, outBkt, overridesBkt, gcsHelper, *osvOutputPath, &successCount) }() } @@ -309,12 +309,12 @@ func main() { if len(v.GetAffected()) > 0 { validIDs = append(validIDs, v.GetId()) } - outputVulnChan <- v + uploadVulnsChan <- v if count%1000 == 0 { logger.Info("Processed CVEs", slog.Int("count", count), slog.Int("total", totalWork), slog.Int("percent", (count*100)/totalWork)) } } - close(outputVulnChan) + close(uploadVulnsChan) }() // Start ReadAndCombineWorkers (Read side)