From 61c15c4da76e5fbf0261c87ddbbb85d5d2d527b6 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Wed, 11 Mar 2026 22:30:45 +0000 Subject: [PATCH 1/9] Make initial changes to the final json files. --- vulnfeeds/conversion/common.go | 73 ++++++++++--- vulnfeeds/conversion/common_test.go | 22 ++-- vulnfeeds/conversion/grouping.go | 149 +++++++++++++++++--------- vulnfeeds/conversion/grouping_test.go | 9 +- vulnfeeds/conversion/nvd/converter.go | 58 +++++++--- vulnfeeds/cves/versions.go | 17 ++- vulnfeeds/git/versions.go | 3 +- vulnfeeds/models/types.go | 12 +++ vulnfeeds/utility/utility.go | 42 +++++--- 9 files changed, 268 insertions(+), 117 deletions(-) diff --git a/vulnfeeds/conversion/common.go b/vulnfeeds/conversion/common.go index c5906bc917b..bc92fd3e089 100644 --- a/vulnfeeds/conversion/common.go +++ b/vulnfeeds/conversion/common.go @@ -174,7 +174,7 @@ func WriteMetricsFile(metrics *models.ConversionMetrics, metricsFile *os.File) e // GitVersionsToCommits examines repos and tries to convert versions to commits by treating them as Git tags. // Returns the resolved ranges, unresolved ranges, and successful repos involved. -func GitVersionsToCommits(versionRanges []*osvschema.Range, repos []string, metrics *models.ConversionMetrics, cache *git.RepoTagsCache) ([]*osvschema.Range, []*osvschema.Range, []string) { +func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []string, metrics *models.ConversionMetrics, cache *git.RepoTagsCache) ([]*osvschema.Range, []models.RangeWithMetadata, []string) { var newVersionRanges []*osvschema.Range unresolvedRanges := versionRanges var successfulRepos []string @@ -192,10 +192,10 @@ func GitVersionsToCommits(versionRanges []*osvschema.Range, repos []string, metr continue } - var stillUnresolvedRanges []*osvschema.Range + var stillUnresolvedRanges []models.RangeWithMetadata for _, vr := range unresolvedRanges { var introduced, fixed, lastAffected string - for _, e := range vr.GetEvents() { + for _, e := range vr.Range.GetEvents() { if e.GetIntroduced() != "" { introduced = e.GetIntroduced() } @@ -236,8 +236,11 @@ func GitVersionsToCommits(versionRanges []*osvschema.Range, repos []string, metr successfulRepos = append(successfulRepos, repo) newVR.Repo = repo newVR.Type = osvschema.Range_GIT - if len(vr.GetEvents()) > 0 { - databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{"versions": vr.GetEvents()}) + if len(vr.Range.GetEvents()) > 0 { + databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{ + "versions": vr.Range.GetEvents(), + "cpe": vr.Metadata.CPE, + }) if err != nil { metrics.AddNote("failed to make database specific: %v", err) } else { @@ -318,7 +321,7 @@ func MergeTwoRanges(range1, range2 *osvschema.Range) (*osvschema.Range, error) { for k, v := range db2.GetFields() { val2 := v.AsInterface() if existing, ok := mergedMap[k]; ok { - mergedVal, err := mergeDatabaseSpecificValues(existing, val2) + mergedVal, err := MergeDatabaseSpecificValues(existing, val2) if err != nil { logger.Info("Failed to merge database specific key", "key", k, "err", err) } @@ -340,18 +343,26 @@ func MergeTwoRanges(range1, range2 *osvschema.Range) (*osvschema.Range, error) { return mergedRange, nil } -// mergeDatabaseSpecificValues is a helper function that recursively merges two +// MergeDatabaseSpecificValues is a helper function that recursively merges two // values from a DatabaseSpecific field. It handles lists (by appending), maps // (by recursively merging keys), and simple strings (by creating a list if they // differ). It returns an error if the types of the two values do not match. -func mergeDatabaseSpecificValues(val1, val2 any) (any, error) { +func MergeDatabaseSpecificValues(val1, val2 any) (any, error) { switch v1 := val1.(type) { case []any: if v2, ok := val2.([]any); ok { - return append(v1, v2...), nil + return deduplicateList(append(v1, v2...)), nil } - return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2) + // Check if the list contains elements of the same type as val2 + if len(v1) > 0 { + if fmt.Sprintf("%T", v1[0]) != fmt.Sprintf("%T", val2) { + return nil, fmt.Errorf("mismatching types: list of %T and %T", v1[0], val2) + } + } + + // Append single value to list + return deduplicateList(append(v1, val2)), nil case map[string]any: if v2, ok := val2.(map[string]any); ok { merged := make(map[string]any) @@ -360,7 +371,7 @@ func mergeDatabaseSpecificValues(val1, val2 any) (any, error) { } for k, v := range v2 { if existing, ok := merged[k]; ok { - mergedVal, err := mergeDatabaseSpecificValues(existing, v) + mergedVal, err := MergeDatabaseSpecificValues(existing, v) if err != nil { return nil, err } @@ -376,15 +387,27 @@ func mergeDatabaseSpecificValues(val1, val2 any) (any, error) { return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2) case string: if v2, ok := val2.(string); ok { - if v1 == v2 { - return v1, nil + return deduplicateList([]any{v1, v2}), nil + } + if v2, ok := val2.([]any); ok { + if len(v2) > 0 { + if _, isString := v2[0].(string); !isString { + return nil, fmt.Errorf("mismatching types: string and list of %T", v2[0]) + } } - - return []any{v1, v2}, nil + return deduplicateList(append([]any{v1}, v2...)), nil } return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2) default: + if v2, ok := val2.([]any); ok { + if len(v2) > 0 { + if fmt.Sprintf("%T", val1) != fmt.Sprintf("%T", v2[0]) { + return nil, fmt.Errorf("mismatching types: %T and list of %T", val1, v2[0]) + } + } + return deduplicateList(append([]any{val1}, v2...)), nil + } if fmt.Sprintf("%T", val1) != fmt.Sprintf("%T", val2) { return nil, fmt.Errorf("mismatching types: %T and %T", val1, val2) } @@ -392,6 +415,24 @@ func mergeDatabaseSpecificValues(val1, val2 any) (any, error) { return val1, nil } - return []any{val1, val2}, nil + return deduplicateList([]any{val1, val2}), nil + } +} + +// deduplicateList removes duplicate comparable elements (like strings) from a list. +func deduplicateList(list []any) []any { + var unique []any + seen := make(map[any]bool) + for _, item := range list { + switch item.(type) { + case string, int, int32, int64, float32, float64, bool: + if !seen[item] { + seen[item] = true + unique = append(unique, item) + } + default: + unique = append(unique, item) + } } + return unique } diff --git a/vulnfeeds/conversion/common_test.go b/vulnfeeds/conversion/common_test.go index 84ca6444c61..2580f43fc08 100644 --- a/vulnfeeds/conversion/common_test.go +++ b/vulnfeeds/conversion/common_test.go @@ -228,10 +228,16 @@ func TestMergeDatabaseSpecificValues(t *testing.T) { want: []any{"a", "b", "c", "d"}, }, { - name: "List and string mismatch", - val1: []any{"a", "b"}, - val2: "c", - wantErr: true, + name: "List and string", + val1: []any{"a", "b"}, + val2: "c", + want: []any{"a", "b", "c"}, + }, + { + name: "String and list", + val1: "a", + val2: []any{"b", "c"}, + want: []any{"a", "b", "c"}, }, { name: "Merge maps", @@ -268,7 +274,7 @@ func TestMergeDatabaseSpecificValues(t *testing.T) { name: "Merge same strings", val1: "value1", val2: "value1", - want: "value1", + want: []any{"value1"}, }, { name: "Merge different strings", @@ -304,13 +310,13 @@ func TestMergeDatabaseSpecificValues(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := mergeDatabaseSpecificValues(tt.val1, tt.val2) + got, err := MergeDatabaseSpecificValues(tt.val1, tt.val2) if (err != nil) != tt.wantErr { - t.Errorf("mergeDatabaseSpecificValues() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("MergeDatabaseSpecificValues() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && !cmp.Equal(got, tt.want) { - t.Errorf("mergeDatabaseSpecificValues() mismatch (-want +got):\n%s", cmp.Diff(tt.want, got)) + t.Errorf("MergeDatabaseSpecificValues() mismatch (-want +got):\n%s", cmp.Diff(tt.want, got)) } }) } diff --git a/vulnfeeds/conversion/grouping.go b/vulnfeeds/conversion/grouping.go index 48f0b4546c9..f8998e6beb2 100644 --- a/vulnfeeds/conversion/grouping.go +++ b/vulnfeeds/conversion/grouping.go @@ -21,67 +21,116 @@ func GroupAffectedRanges(affected []*osvschema.Affected) { continue } - // Key for grouping: Type + Repo + Introduced Value - type groupKey struct { - RangeType osvschema.Range_Type - Repo string - Introduced string - } + aff.Ranges = GroupRanges(aff.GetRanges()) + } +} - groups := make(map[groupKey]*osvschema.Range) - var order []groupKey // To maintain deterministic order of first appearance - - for _, r := range aff.GetRanges() { - // Find the introduced event - var introduced string - var introducedCount int - for _, e := range r.GetEvents() { - if e.GetIntroduced() != "" { - introduced = e.GetIntroduced() - introducedCount++ - } - } +func GroupRanges(ranges []*osvschema.Range) []*osvschema.Range { + // Key for grouping: Type + Repo + Introduced Value + type groupKey struct { + RangeType osvschema.Range_Type + Repo string + Introduced string + } - if introducedCount > 1 { - logger.Error("Multiple 'introduced' events found in a single range", slog.Any("range", r)) - } + groups := make(map[groupKey]*osvschema.Range) + var order []groupKey // To maintain deterministic order of first appearance - // If no introduced event is found, we use an empty string as the introduced value. - key := groupKey{ - RangeType: r.GetType(), - Repo: r.GetRepo(), - Introduced: introduced, + for _, r := range ranges { + // Find the introduced event + var introduced string + var introducedCount int + for _, e := range r.GetEvents() { + if e.GetIntroduced() != "" { + introduced = e.GetIntroduced() + introducedCount++ } + } - if _, exists := groups[key]; !exists { - // Initialize with a deep copy of the first range found for this group - // We need to be careful about DatabaseSpecific. - // We want to keep the "versions" from this first range. - groups[key] = &osvschema.Range{ - Type: r.GetType(), - Repo: r.GetRepo(), - Events: []*osvschema.Event{}, - DatabaseSpecific: r.GetDatabaseSpecific(), // Start with this one's DS - } - order = append(order, key) - } else { - // Merge DatabaseSpecific "versions" - mergeDatabaseSpecificVersions(groups[key], r.GetDatabaseSpecific()) + if introducedCount > 1 { + logger.Error("Multiple 'introduced' events found in a single range", slog.Any("range", r)) + } + + // If no introduced event is found, we use an empty string as the introduced value. + key := groupKey{ + RangeType: r.GetType(), + Repo: r.GetRepo(), + Introduced: introduced, + } + + if _, exists := groups[key]; !exists { + // Initialize with a deep copy of the first range found for this group + // We need to be careful about DatabaseSpecific. + // We want to keep the "versions" from this first range. + groups[key] = &osvschema.Range{ + Type: r.GetType(), + Repo: r.GetRepo(), + Events: []*osvschema.Event{}, + DatabaseSpecific: r.GetDatabaseSpecific(), // Start with this one's DS } + order = append(order, key) + } else { + // Merge DatabaseSpecific + mergeDatabaseSpecific(groups[key], r.GetDatabaseSpecific()) + } + + // Add all events to the group. Deduplication happens later in cleanEvents. + groups[key].Events = append(groups[key].Events, r.GetEvents()...) + } + + // Reconstruct ranges from groups + var newRanges []*osvschema.Range + for _, key := range order { + r := groups[key] + r.Events = cleanEvents(r.GetEvents()) + newRanges = append(newRanges, r) + } + return newRanges +} + +// mergeDatabaseSpecific merges the source DatabaseSpecific into the target DatabaseSpecific. +// It uses MergeDatabaseSpecificValues for all fields except "versions", which is handled +// by mergeDatabaseSpecificVersions for deduplication. +func mergeDatabaseSpecific(target *osvschema.Range, source *structpb.Struct) { + if source == nil { + return + } - // Add all events to the group. Deduplication happens later in cleanEvents. - groups[key].Events = append(groups[key].Events, r.GetEvents()...) + if target.GetDatabaseSpecific() == nil { + var err error + target.DatabaseSpecific, err = structpb.NewStruct(nil) + if err != nil { + logger.Fatal("Failed to create DatabaseSpecific", slog.Any("error", err)) } + } - // Reconstruct ranges from groups - var newRanges []*osvschema.Range - for _, key := range order { - r := groups[key] - r.Events = cleanEvents(r.GetEvents()) - newRanges = append(newRanges, r) + targetFields := target.GetDatabaseSpecific().GetFields() + if targetFields == nil { + targetFields = make(map[string]*structpb.Value) + target.DatabaseSpecific.Fields = targetFields + } + + for k, v := range source.GetFields() { + if k == "versions" { + continue // Handled separately + } + val2 := v.AsInterface() + if existing, ok := targetFields[k]; ok { + mergedVal, err := MergeDatabaseSpecificValues(existing.AsInterface(), val2) + if err != nil { + logger.Info("Failed to merge database specific key", "key", k, "err", err) + } + if newVal, err := structpb.NewValue(mergedVal); err == nil { + targetFields[k] = newVal + } else { + logger.Warn("Failed to create structpb.Value for merged key", "key", k, "err", err) + } + } else { + targetFields[k] = v } - aff.Ranges = newRanges } + + mergeDatabaseSpecificVersions(target, source) } // mergeDatabaseSpecificVersions merges the "versions" field from the source DatabaseSpecific diff --git a/vulnfeeds/conversion/grouping_test.go b/vulnfeeds/conversion/grouping_test.go index 62d057dcc94..feb1a8173ea 100644 --- a/vulnfeeds/conversion/grouping_test.go +++ b/vulnfeeds/conversion/grouping_test.go @@ -237,7 +237,7 @@ func TestGroupAffectedRanges(t *testing.T) { }, }, { - name: "Different DatabaseSpecific (non-versions) - merge, second gets overwritten", + name: "Different DatabaseSpecific (non-versions) - merge properly", affected: []*osvschema.Affected{ { Ranges: []*osvschema.Range{ @@ -280,7 +280,12 @@ func TestGroupAffectedRanges(t *testing.T) { }, DatabaseSpecific: &structpb.Struct{ Fields: map[string]*structpb.Value{ - "foo": structpb.NewStringValue("bar"), + "foo": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{ + structpb.NewStringValue("bar"), + structpb.NewStringValue("baz"), + }, + }), }, }, }, diff --git a/vulnfeeds/conversion/nvd/converter.go b/vulnfeeds/conversion/nvd/converter.go index 0c1e176b14e..d46b6fbd693 100644 --- a/vulnfeeds/conversion/nvd/converter.go +++ b/vulnfeeds/conversion/nvd/converter.go @@ -19,6 +19,7 @@ import ( "github.com/google/osv/vulnfeeds/utility/logger" "github.com/google/osv/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" + "google.golang.org/protobuf/types/known/structpb" ) var ErrNoRanges = errors.New("no ranges") @@ -59,14 +60,19 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc } successfulRepos := make(map[string]bool) - var resolvedRanges, unresolvedRanges []*osvschema.Range + var resolvedRanges []*osvschema.Range + var unresolvedRanges []models.RangeWithMetadata // Exit early if there are no repositories if len(repos) == 0 { metrics.SetOutcome(models.NoRepos) metrics.UnresolvedRangesCount += len(cpeRanges) - affected := MergeRangesAndCreateAffected(resolvedRanges, cpeRanges, nil, nil, metrics) - v.Affected = append(v.Affected, affected) + + unresolvedDatabaseSpecificField := createUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) + if unresolvedDatabaseSpecificField != nil { + v.DatabaseSpecific = unresolvedDatabaseSpecificField + } + // Exit early outputFiles(v, directory, maybeVendorName, maybeProductName, metrics, rejectFailed, outputMetrics) @@ -116,9 +122,16 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc // Use the successful repos for more efficient merging. keys := slices.Collect(maps.Keys(successfulRepos)) - affected := MergeRangesAndCreateAffected(resolvedRanges, unresolvedRanges, commits, keys, metrics) + groupedRanges := conversion.GroupRanges(resolvedRanges) + affected := MergeRangesAndCreateAffected(groupedRanges, commits, keys, metrics) v.Affected = append(v.Affected, affected) + unresolvedDatabaseSpecificField := createUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) + // TODO: this should be if v.DatabaseSpecific != nil, initalise, otherwise add it. + if unresolvedDatabaseSpecificField != nil { + v.DatabaseSpecific = unresolvedDatabaseSpecificField + } + if !outputMetrics && rejectFailed && metrics.Outcome != models.Successful { return metrics.Outcome } @@ -313,11 +326,10 @@ func FindRepos(cve models.NVDCVE, vpRepoCache *cves.VPRepoCache, repoTagsCache * // // Arguments: // - resolvedRanges: A slice of resolved OSV ranges to be merged. -// - unresolvedRanges: A slice of unresolved OSV ranges to be included in the database specific field. // - commits: A slice of affected commits to be converted into events and added to ranges. // - successfulRepos: A slice of repository URLs that were successfully processed. // - metrics: A pointer to ConversionMetrics to track the outcome and notes. -func MergeRangesAndCreateAffected(resolvedRanges []*osvschema.Range, unresolvedRanges []*osvschema.Range, commits []models.AffectedCommit, successfulRepos []string, metrics *models.ConversionMetrics) *osvschema.Affected { +func MergeRangesAndCreateAffected(resolvedRanges []*osvschema.Range, commits []models.AffectedCommit, successfulRepos []string, metrics *models.ConversionMetrics) *osvschema.Affected { var newResolvedRanges []*osvschema.Range // Combine the ranges appropriately if len(resolvedRanges) > 0 { @@ -371,14 +383,6 @@ func MergeRangesAndCreateAffected(resolvedRanges []*osvschema.Range, unresolvedR Ranges: newResolvedRanges, } - if len(unresolvedRanges) > 0 { - databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{"unresolved_ranges": unresolvedRanges}) - if err != nil { - metrics.AddNote("failed to make database specific: %v", err) - } - newAffected.DatabaseSpecific = databaseSpecific - } - return newAffected } @@ -476,7 +480,7 @@ func outputFiles(v *vulns.Vulnerability, dir string, vendor string, product stri } // processRanges attempts to resolve the given ranges to commits and updates the metrics accordingly. -func processRanges(ranges []*osvschema.Range, repos []string, metrics *models.ConversionMetrics, cache *git.RepoTagsCache, source models.VersionSource) ([]*osvschema.Range, []*osvschema.Range, []string) { +func processRanges(ranges []models.RangeWithMetadata, repos []string, metrics *models.ConversionMetrics, cache *git.RepoTagsCache, source models.VersionSource) ([]*osvschema.Range, []models.RangeWithMetadata, []string) { if len(ranges) == 0 { return nil, nil, nil } @@ -498,3 +502,27 @@ func processRanges(ranges []*osvschema.Range, repos []string, metrics *models.Co return r, un, sR } + +func createUnresolvedDatabaseSpecificField(unresolvedRanges []models.RangeWithMetadata, metrics *models.ConversionMetrics) *structpb.Struct { + if len(unresolvedRanges) > 0 { + var unresolvedRangesMap []map[string]any + for _, ur := range unresolvedRanges { + urMap := map[string]any{ + "range": ur.Range, + "metadata": map[string]any{ + "cpe": ur.Metadata.CPE, + }, + } + unresolvedRangesMap = append(unresolvedRangesMap, urMap) + } + databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{ + "unresolved_ranges": unresolvedRangesMap, + }) + if err != nil { + metrics.AddNote("failed to make database specific: %v", err) + } + return databaseSpecific + } + + return nil +} diff --git a/vulnfeeds/cves/versions.go b/vulnfeeds/cves/versions.go index fcac925deeb..c335f85b686 100644 --- a/vulnfeeds/cves/versions.go +++ b/vulnfeeds/cves/versions.go @@ -29,7 +29,6 @@ import ( "time" "github.com/knqyf263/go-cpe/naming" - "github.com/ossf/osv-schema/bindings/go/osvschema" "github.com/sethvargo/go-retry" "github.com/google/osv/vulnfeeds/conversion" @@ -199,7 +198,7 @@ func Repo(u string) (string, error) { return fmt.Sprintf("%s://%s/%s", parsedURL.Scheme, parsedURL.Hostname(), pathParts[2]), nil } if parsedURL.Hostname() == "sourceware.org" { - // Call out to models function for GitWeb URLs + // Call out to m function for GitWeb URLs return repoGitWeb(parsedURL) } if parsedURL.Hostname() == "git.postgresql.org" { @@ -657,7 +656,7 @@ func processExtractedVersion(version string) string { return version } -func ExtractVersionsFromText(validVersions []string, text string, metrics *models.ConversionMetrics) []*osvschema.Range { +func ExtractVersionsFromText(validVersions []string, text string, metrics *models.ConversionMetrics) []models.RangeWithMetadata { // Match: // - x.x.x before x.x.x // - x.x.x through x.x.x @@ -670,14 +669,14 @@ func ExtractVersionsFromText(validVersions []string, text string, metrics *model return nil } - versions := make([]*osvschema.Range, 0, len(matches)) + versions := make([]models.RangeWithMetadata, 0, len(matches)) for _, match := range matches { // Trim periods that are part of sentences. introduced := processExtractedVersion(match[1]) fixed := processExtractedVersion(match[3]) lastaffected := "" - if match[2] == "through" { + if match[2] == "through" && validVersions != nil { // "Through" implies inclusive range, so the fixed version is the one that comes after. var err error fixed, err = nextVersion(validVersions, fixed) @@ -709,7 +708,7 @@ func ExtractVersionsFromText(validVersions []string, text string, metrics *model } vr := conversion.BuildVersionRange(introduced, lastaffected, fixed) - versions = append(versions, vr) + versions = append(versions, models.RangeWithMetadata{Range: vr}) } return versions @@ -735,8 +734,8 @@ func DeduplicateAffectedCommits(commits []models.AffectedCommit) []models.Affect return uniqueCommits } -func ExtractVersionsFromCPEs(cve models.NVDCVE, validVersions []string, metrics *models.ConversionMetrics) []*osvschema.Range { - versions := []*osvschema.Range{} +func ExtractVersionsFromCPEs(cve models.NVDCVE, validVersions []string, metrics *models.ConversionMetrics) []models.RangeWithMetadata { + versions := []models.RangeWithMetadata{} for _, config := range cve.Configurations { for _, node := range config.Nodes { @@ -815,7 +814,7 @@ func ExtractVersionsFromCPEs(cve models.NVDCVE, validVersions []string, metrics metrics.AddNote("Warning: %s is not a valid fixed version", fixed) } vr := conversion.BuildVersionRange(introduced, lastaffected, fixed) - versions = append(versions, vr) + versions = append(versions, models.RangeWithMetadata{Range: vr, Metadata: models.Metadata{CPE: match.Criteria}}) } } } diff --git a/vulnfeeds/git/versions.go b/vulnfeeds/git/versions.go index 3983cc651c1..e7e3a38013b 100644 --- a/vulnfeeds/git/versions.go +++ b/vulnfeeds/git/versions.go @@ -15,7 +15,6 @@ package git import ( - "errors" "fmt" "regexp" "slices" @@ -84,7 +83,7 @@ func VersionToAffectedCommit(version string, repo string, commitType models.Comm // Take an unnormalized version string, the pre-normalized mapping of tags to commits and return a commit hash. func VersionToCommit(version string, normalizedTags map[string]NormalizedTag) (string, error) { if version == "" { - return "", errors.New("version cannot be empty") + return "", nil } // TODO: try unnormalized version first. normalizedVersion, err := NormalizeVersion(version) diff --git a/vulnfeeds/models/types.go b/vulnfeeds/models/types.go index ab1bb500b32..27bb36055ff 100644 --- a/vulnfeeds/models/types.go +++ b/vulnfeeds/models/types.go @@ -5,6 +5,8 @@ import ( "cmp" "reflect" "strings" + + "github.com/ossf/osv-schema/bindings/go/osvschema" ) type AffectedCommit struct { @@ -29,6 +31,16 @@ func SetCommitByType(ac *AffectedCommit, commitType CommitType, commitHash strin } } +type RangeWithMetadata struct { + Range *osvschema.Range + Metadata Metadata +} + +type Metadata struct { + CPE string +} + + func (ac *AffectedCommit) SetRepo(repo string) { // GitHub.com repos are demonstrably case-insensitive, and frequently // expressed in URLs with varying cases, so normalize them to lowercase. diff --git a/vulnfeeds/utility/utility.go b/vulnfeeds/utility/utility.go index bbbe3956ae4..8e0db1c8e5d 100644 --- a/vulnfeeds/utility/utility.go +++ b/vulnfeeds/utility/utility.go @@ -74,6 +74,14 @@ func newStructpbValue(v any) (*structpb.Value, error) { return structpb.NewNullValue(), nil } + if msg, ok := v.(proto.Message); ok { + val, err := protoToAny(msg) + if err != nil { + return nil, fmt.Errorf("failed to convert proto message: %w", err) + } + return structpb.NewValue(val) + } + val := reflect.ValueOf(v) switch val.Kind() { case reflect.Slice: @@ -83,6 +91,19 @@ func newStructpbValue(v any) (*structpb.Value, error) { } return structpbValueFromList(anyList) + case reflect.Map: + if val.Type().Key().Kind() == reflect.String { + m := make(map[string]any) + for _, k := range val.MapKeys() { + m[k.String()] = val.MapIndex(k).Interface() + } + structpbMap, err := NewStructpbFromMap(m) + if err != nil { + return nil, err + } + return structpb.NewStructValue(structpbMap), nil + } + return structpb.NewValue(v) default: return structpb.NewValue(v) } @@ -93,25 +114,16 @@ func newStructpbValue(v any) (*structpb.Value, error) { // It iterates through the list, converting any proto.Message elements into // any type via JSON marshalling, while other elements are included as-is. func structpbValueFromList(list []any) (*structpb.Value, error) { - anyList := make([]any, 0, len(list)) + var values []*structpb.Value for i, v := range list { - if msg, ok := v.(proto.Message); ok { - val, err := protoToAny(msg) - if err != nil { - return nil, fmt.Errorf("failed to convert proto message for item %d: %w", i, err) - } - anyList = append(anyList, val) - } else { - anyList = append(anyList, v) + val, err := newStructpbValue(v) + if err != nil { + return nil, fmt.Errorf("failed to convert item %d: %w", i, err) } + values = append(values, val) } - val, err := structpb.NewValue(anyList) - if err != nil { - return nil, fmt.Errorf("failed to create new structpb.Value from list: %w", err) - } - - return val, nil + return structpb.NewListValue(&structpb.ListValue{Values: values}), nil } // protoToAny converts a proto.Message to an any type by marshalling to JSON From eaaf820debe1c557b5e2902f49390b95bf5eef32 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 12 Mar 2026 02:26:03 +0000 Subject: [PATCH 2/9] cache canonicalizing link --- vulnfeeds/conversion/common.go | 9 ++++ vulnfeeds/conversion/nvd/converter.go | 6 +-- vulnfeeds/cves/versions.go | 51 +-------------------- vulnfeeds/cves/versions_test.go | 54 ---------------------- vulnfeeds/git/repository.go | 21 ++++++++- vulnfeeds/git/versions.go | 64 +++++++++++++++++++++++++++ vulnfeeds/git/versions_test.go | 55 +++++++++++++++++++++++ 7 files changed, 153 insertions(+), 107 deletions(-) diff --git a/vulnfeeds/conversion/common.go b/vulnfeeds/conversion/common.go index bc92fd3e089..bd4e3883f7e 100644 --- a/vulnfeeds/conversion/common.go +++ b/vulnfeeds/conversion/common.go @@ -9,6 +9,7 @@ import ( "fmt" "io/fs" "log/slog" + "net/http" "os" "path/filepath" "slices" @@ -64,6 +65,7 @@ func AddAffected(v *vulns.Vulnerability, aff *osvschema.Affected, metrics *model } func DeduplicateRefs(refs []models.Reference) []models.Reference { + refs = slices.Clone(refs) // Deduplicate references by URL. slices.SortStableFunc(refs, func(a, b models.Reference) int { return strings.Compare(a.URL, b.URL) @@ -186,6 +188,13 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri if cache.IsInvalid(repo) { continue } + + repo, err := git.FindCanonicalLink(repo, http.DefaultClient, cache) + if err != nil { + metrics.AddNote("Failed to find canonical link - %s", repo) + continue + } + normalizedTags, err := git.NormalizeRepoTags(repo, cache) if err != nil { metrics.AddNote("Failed to normalize tags - %s", repo) diff --git a/vulnfeeds/conversion/nvd/converter.go b/vulnfeeds/conversion/nvd/converter.go index d46b6fbd693..0df1ffeb121 100644 --- a/vulnfeeds/conversion/nvd/converter.go +++ b/vulnfeeds/conversion/nvd/converter.go @@ -30,6 +30,7 @@ var ErrUnresolvedFix = errors.New("fixes not resolved to commits") func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, directory string, metrics *models.ConversionMetrics, rejectFailed bool, outputMetrics bool) models.ConversionOutcome { CPEs := cves.CPEs(cve) metrics.CPEs = CPEs + refs := conversion.DeduplicateRefs(cve.References) // 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" @@ -88,7 +89,7 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc } // Extract Commits - commits, err := cves.ExtractCommitsFromRefs(cve.References, http.DefaultClient) + commits, err := cves.ExtractCommitsFromRefs(refs, http.DefaultClient) if err != nil { metrics.AddNote("Failed to extract commits from refs: %#v", err) } @@ -245,8 +246,7 @@ func CVEToPackageInfo(cve models.NVDCVE, repos []string, cache *git.RepoTagsCach // FindRepos attempts to find the source code repositories for a given CVE. func FindRepos(cve models.NVDCVE, vpRepoCache *cves.VPRepoCache, repoTagsCache *git.RepoTagsCache, metrics *models.ConversionMetrics, httpClient *http.Client) []string { // Find repos - refs := cve.References - conversion.DeduplicateRefs(refs) + refs := conversion.DeduplicateRefs(cve.References) CPEs := cves.CPEs(cve) CVEID := cve.ID var reposForCVE []string diff --git a/vulnfeeds/cves/versions.go b/vulnfeeds/cves/versions.go index c335f85b686..6273965da30 100644 --- a/vulnfeeds/cves/versions.go +++ b/vulnfeeds/cves/versions.go @@ -16,7 +16,6 @@ package cves import ( - "context" "errors" "fmt" "net/http" @@ -26,10 +25,8 @@ import ( "slices" "strings" "sync" - "time" "github.com/knqyf263/go-cpe/naming" - "github.com/sethvargo/go-retry" "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/git" @@ -504,50 +501,6 @@ func resolveGitTag(parsedURL *url.URL, u string, gitSHA1Regex *regexp.Regexp) (s return "", errors.New("no tag found") } -// Detect linkrot and handle link decay in HTTP(S) links via HEAD request with exponential backoff. -func ValidateAndCanonicalizeLink(link string, httpClient *http.Client) (canonicalLink string, err error) { - u, err := url.Parse(link) - if !slices.Contains([]string{"http", "https"}, u.Scheme) { - // Handle what's presumably a git:// URL. - return link, err - } - backoff := retry.NewExponential(1 * time.Second) - if err := retry.Do(context.Background(), retry.WithMaxRetries(3, backoff), func(ctx context.Context) error { - req, err := http.NewRequestWithContext(ctx, http.MethodHead, link, nil) - if err != nil { - return err - } - - // security.alpinelinux.org responds with text/html content. - // default HEAD request in Go does not provide any Accept headers, causing a 406 response. - req.Header.Set("Accept", "text/html") - - // Send the request - resp, err := httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - switch resp.StatusCode / 100 { - // 4xx response codes are an instant fail. - case 4: - return fmt.Errorf("bad response: %v", resp.StatusCode) - // 5xx response codes are retriable. - case 5: - return retry.RetryableError(fmt.Errorf("bad response: %v", resp.StatusCode)) - // Anything else is acceptable. - default: - canonicalLink = resp.Request.URL.String() - return nil - } - }); err != nil { - return link, fmt.Errorf("unable to determine validity of %q: %w", link, err) - } - - return canonicalLink, nil -} - // For URLs referencing commits in supported Git repository hosts, return a cloneable AffectedCommit. func ExtractCommitsFromRefs(references []models.Reference, httpClient *http.Client) ([]models.AffectedCommit, error) { var commits []models.AffectedCommit //nolint:prealloc @@ -599,7 +552,7 @@ func ExtractGitCommit(link string, httpClient *http.Client, depth int) (string, commit = c // If URL doesn't validate, treat it as linkrot. - possiblyDifferentLink, err := ValidateAndCanonicalizeLink(link, httpClient) + possiblyDifferentLink, err := git.ValidateAndCanonicalizeLink(link, httpClient) if err != nil { return "", "", err } @@ -1191,7 +1144,7 @@ func ReposFromReferences(cache *VPRepoCache, vp *VendorProduct, refs []models.Re } // Check if the repo URL has changed (e.g. via redirect) - canonicalRepo, err := ValidateAndCanonicalizeLink(repo, httpClient) + canonicalRepo, err := git.FindCanonicalLink(repo, httpClient, repoTagsCache) if err == nil { repo = canonicalRepo } diff --git a/vulnfeeds/cves/versions_test.go b/vulnfeeds/cves/versions_test.go index a1e293d0a60..b32f91035bc 100644 --- a/vulnfeeds/cves/versions_test.go +++ b/vulnfeeds/cves/versions_test.go @@ -1166,60 +1166,6 @@ func TestInvalidRangeDetection(t *testing.T) { } } -func TestValidateAndCanonicalizeLink(t *testing.T) { - type args struct { - link string - } - tests := []struct { - name string - args args - wantCanonicalLink string - wantErr bool - skipOnCloudBuild bool - disableExpiryDate time.Time // If test needs to be disabled due to known outage. - }{ - { - name: "A link that 404's", - args: args{ - link: "https://github.com/WebKit/webkit/commit/6f9b511a115311b13c06eb58038ddc2c78da5531", - }, - wantCanonicalLink: "https://github.com/WebKit/webkit/commit/6f9b511a115311b13c06eb58038ddc2c78da5531", - wantErr: true, - }, - { - name: "A functioning link", - args: args{ - link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ee1fee900537b5d9560e9f937402de5ddc8412f3", - }, - wantCanonicalLink: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ee1fee900537b5d9560e9f937402de5ddc8412f3", - wantErr: false, - skipOnCloudBuild: true, // observing indications of IP denylisting as at 2025-02-13 - - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := testutils.SetupVCR(t) - client := r.GetDefaultClient() - - if time.Now().Before(tt.disableExpiryDate) { - t.Skipf("test %q has been skipped due to known outage and will be reenabled on %s.", tt.name, tt.disableExpiryDate) - } - if _, ok := os.LookupEnv("BUILD_ID"); ok && tt.skipOnCloudBuild { - t.Skipf("test %q: running on Cloud Build", tt.name) - } - gotCanonicalLink, err := ValidateAndCanonicalizeLink(tt.args.link, client) - if (err != nil) != tt.wantErr { - t.Errorf("ValidateAndCanonicalizeLink() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotCanonicalLink != tt.wantCanonicalLink { - t.Errorf("ValidateAndCanonicalizeLink() = %v, want %v", gotCanonicalLink, tt.wantCanonicalLink) - } - }) - } -} - func TestCommit(t *testing.T) { type args struct { u string diff --git a/vulnfeeds/git/repository.go b/vulnfeeds/git/repository.go index 776d630b5a7..424dedafccc 100644 --- a/vulnfeeds/git/repository.go +++ b/vulnfeeds/git/repository.go @@ -73,7 +73,7 @@ type RepoTagsCache struct { m map[string]RepoTagsMap invalid map[string]bool -} + canonicalLink map[string]string} func (c *RepoTagsCache) Get(repo string) (RepoTagsMap, bool) { c.RLock() @@ -114,6 +114,25 @@ func (c *RepoTagsCache) IsInvalid(repo string) bool { return c.invalid[repo] } +func (c *RepoTagsCache) SetCanonicalLink(repo string, canonicalLink string) { + c.Lock() + defer c.Unlock() + if c.canonicalLink == nil { + c.canonicalLink = make(map[string]string) + } + c.canonicalLink[repo] = canonicalLink +} + +func (c *RepoTagsCache) GetCanonicalLink(repo string) (string, bool) { + c.RLock() + defer c.RUnlock() + if c.canonicalLink == nil { + return "", false + } + canonicalLink, ok := c.canonicalLink[repo] + return canonicalLink, ok +} + // RemoteRepoRefsWithRetry will exponentially retry listing the peeled references of the repoURL up to retries times. func RemoteRepoRefsWithRetry(repoURL string, retries uint64) (refs []*plumbing.Reference, err error) { remoteConfig := &config.RemoteConfig{ diff --git a/vulnfeeds/git/versions.go b/vulnfeeds/git/versions.go index e7e3a38013b..2600785fc28 100644 --- a/vulnfeeds/git/versions.go +++ b/vulnfeeds/git/versions.go @@ -15,12 +15,17 @@ package git import ( + "context" "fmt" + "net/http" + "net/url" "regexp" "slices" "strings" + "time" "github.com/google/osv/vulnfeeds/models" + "github.com/sethvargo/go-retry" ) var ( @@ -178,3 +183,62 @@ func ParseVersionRange(versionRange string) (models.AffectedVersion, error) { return av, nil } + +// Detect linkrot and handle link decay in HTTP(S) links via HEAD request with exponential backoff. +func ValidateAndCanonicalizeLink(link string, httpClient *http.Client) (canonicalLink string, err error) { + u, err := url.Parse(link) + if !slices.Contains([]string{"http", "https"}, u.Scheme) { + // Handle what's presumably a git:// URL. + return link, err + } + if httpClient == nil { + httpClient = http.DefaultClient + } + backoff := retry.NewExponential(1 * time.Second) + if err := retry.Do(context.Background(), retry.WithMaxRetries(3, backoff), func(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, link, nil) + if err != nil { + return err + } + + // security.alpinelinux.org responds with text/html content. + // default HEAD request in Go does not provide any Accept headers, causing a 406 response. + req.Header.Set("Accept", "text/html") + + // Send the request + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + switch resp.StatusCode / 100 { + // 4xx response codes are an instant fail. + case 4: + return fmt.Errorf("bad response: %v", resp.StatusCode) + // 5xx response codes are retriable. + case 5: + return retry.RetryableError(fmt.Errorf("bad response: %v", resp.StatusCode)) + // Anything else is acceptable. + default: + canonicalLink = resp.Request.URL.String() + return nil + } + }); err != nil { + return link, fmt.Errorf("unable to determine validity of %q: %w", link, err) + } + + return canonicalLink, nil +} + +func FindCanonicalLink(link string, httpClient *http.Client, cache *RepoTagsCache) (canonicalLink string, err error) { + if canonicalLink, ok := cache.GetCanonicalLink(link); ok { + return canonicalLink, nil + } + canonicalLink, err = ValidateAndCanonicalizeLink(link, httpClient) + if err != nil { + return "", err + } + cache.SetCanonicalLink(link, canonicalLink) + return canonicalLink, nil +} \ No newline at end of file diff --git a/vulnfeeds/git/versions_test.go b/vulnfeeds/git/versions_test.go index 39e14e206a1..d23de7adcc9 100644 --- a/vulnfeeds/git/versions_test.go +++ b/vulnfeeds/git/versions_test.go @@ -1,6 +1,7 @@ package git import ( + "os" "reflect" "testing" "time" @@ -323,3 +324,57 @@ func TestParseVersionRange(t *testing.T) { }) } } + +func TestValidateAndCanonicalizeLink(t *testing.T) { + type args struct { + link string + } + tests := []struct { + name string + args args + wantCanonicalLink string + wantErr bool + skipOnCloudBuild bool + disableExpiryDate time.Time // If test needs to be disabled due to known outage. + }{ + { + name: "A link that 404's", + args: args{ + link: "https://github.com/WebKit/webkit/commit/6f9b511a115311b13c06eb58038ddc2c78da5531", + }, + wantCanonicalLink: "https://github.com/WebKit/webkit/commit/6f9b511a115311b13c06eb58038ddc2c78da5531", + wantErr: true, + }, + { + name: "A functioning link", + args: args{ + link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ee1fee900537b5d9560e9f937402de5ddc8412f3", + }, + wantCanonicalLink: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ee1fee900537b5d9560e9f937402de5ddc8412f3", + wantErr: false, + skipOnCloudBuild: true, // observing indications of IP denylisting as at 2025-02-13 + + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := testutils.SetupVCR(t) + client := r.GetDefaultClient() + + if time.Now().Before(tt.disableExpiryDate) { + t.Skipf("test %q has been skipped due to known outage and will be reenabled on %s.", tt.name, tt.disableExpiryDate) + } + if _, ok := os.LookupEnv("BUILD_ID"); ok && tt.skipOnCloudBuild { + t.Skipf("test %q: running on Cloud Build", tt.name) + } + gotCanonicalLink, err := ValidateAndCanonicalizeLink(tt.args.link, client) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateAndCanonicalizeLink() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotCanonicalLink != tt.wantCanonicalLink { + t.Errorf("ValidateAndCanonicalizeLink() = %v, want %v", gotCanonicalLink, tt.wantCanonicalLink) + } + }) + } +} \ No newline at end of file From 6ad605f75e0b471d69130a9ca5ac3dfe0a56b079 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 16 Mar 2026 00:31:42 +0000 Subject: [PATCH 3/9] add interoperability with cve5 records too --- vulnfeeds/conversion/common.go | 96 ++++++++++++++++++- vulnfeeds/conversion/grouping.go | 3 +- vulnfeeds/conversion/nvd/converter.go | 68 ++----------- vulnfeeds/cvelist2osv/default_extractor.go | 58 +++++++---- vulnfeeds/cvelist2osv/strategies.go | 14 --- vulnfeeds/git/repository.go | 8 +- ...ndCanonicalizeLink_A_functioning_link.yaml | 51 ++++++++++ ...AndCanonicalizeLink_A_link_that_404_s.yaml | 55 +++++++++++ vulnfeeds/git/versions.go | 3 +- vulnfeeds/git/versions_test.go | 2 +- vulnfeeds/models/types.go | 3 +- vulnfeeds/utility/utility.go | 5 +- 12 files changed, 259 insertions(+), 107 deletions(-) create mode 100644 vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_functioning_link.yaml create mode 100644 vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_link_that_404_s.yaml diff --git a/vulnfeeds/conversion/common.go b/vulnfeeds/conversion/common.go index c340d9d2a24..d376a6bce18 100644 --- a/vulnfeeds/conversion/common.go +++ b/vulnfeeds/conversion/common.go @@ -22,6 +22,7 @@ import ( "github.com/google/osv/vulnfeeds/utility/logger" "github.com/google/osv/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" + "google.golang.org/protobuf/types/known/structpb" ) // AddAffected adds an osvschema.Affected to a vulnerability, ensuring that no duplicate ranges are added. @@ -189,7 +190,7 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri if cache.IsInvalid(repo) { continue } - + repo, err := git.FindCanonicalLink(repo, http.DefaultClient, cache) if err != nil { metrics.AddNote("Failed to find canonical link - %s %v", repo, err) @@ -197,6 +198,7 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri metrics.Outcome = models.Error return nil, nil, nil } + continue } @@ -256,10 +258,13 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri newVR.Repo = repo newVR.Type = osvschema.Range_GIT if len(vr.Range.GetEvents()) > 0 { - databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{ + dbSpecificMap := map[string]any{ "versions": vr.Range.GetEvents(), - "cpe": vr.Metadata.CPE, - }) + } + if vr.Metadata.CPE != "" { + dbSpecificMap["cpe"] = vr.Metadata.CPE + } + databaseSpecific, err := utility.NewStructpbFromMap(dbSpecificMap) if err != nil { metrics.AddNote("failed to make database specific: %v", err) } else { @@ -414,6 +419,7 @@ func MergeDatabaseSpecificValues(val1, val2 any) (any, error) { return nil, fmt.Errorf("mismatching types: string and list of %T", v2[0]) } } + return deduplicateList(append([]any{v1}, v2...)), nil } @@ -425,6 +431,7 @@ func MergeDatabaseSpecificValues(val1, val2 any) (any, error) { return nil, fmt.Errorf("mismatching types: %T and list of %T", val1, v2[0]) } } + return deduplicateList(append([]any{val1}, v2...)), nil } if fmt.Sprintf("%T", val1) != fmt.Sprintf("%T", val2) { @@ -453,5 +460,86 @@ func deduplicateList(list []any) []any { unique = append(unique, item) } } + return unique } + +func CreateUnresolvedDatabaseSpecificField(unresolvedRanges []models.RangeWithMetadata, metrics *models.ConversionMetrics) *structpb.Struct { + if len(unresolvedRanges) > 0 { + var unresolvedRangesMap []map[string]any + for _, ur := range unresolvedRanges { + urMap := map[string]any{ + "range": ur.Range, + } + if ur.Metadata.CPE != "" { + urMap["metadata"] = map[string]any{ + "cpe": ur.Metadata.CPE, + } + } + unresolvedRangesMap = append(unresolvedRangesMap, urMap) + } + databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{ + "unresolved_ranges": unresolvedRangesMap, + }) + if err != nil { + metrics.AddNote("failed to make database specific: %v", err) + } + + return databaseSpecific + } + + return nil +} + +func AddFieldToDatabaseSpecific(ds *structpb.Struct, field string, value any) error { + if ds == nil { + return errors.New("database specific is nil") + } + if ds.Fields == nil { + return errors.New("database specific fields is nil") + } + if ds.GetFields()[field] != nil { + return fmt.Errorf("field %s already exists", field) + } + + switch v := value.(type) { + case *structpb.Value: + ds.Fields[field] = v + case *structpb.Struct: + ds.Fields[field] = structpb.NewStructValue(v) + case *structpb.ListValue: + ds.Fields[field] = structpb.NewListValue(v) + default: + val, err := structpb.NewValue(v) + if err != nil { + return fmt.Errorf("failed to create structpb value: %w", err) + } + ds.Fields[field] = val + } + + return nil +} + +// ProcessRanges attempts to resolve the given ranges to commits and updates the metrics accordingly. +func ProcessRanges(ranges []models.RangeWithMetadata, repos []string, metrics *models.ConversionMetrics, cache *git.RepoTagsCache, source models.VersionSource) ([]*osvschema.Range, []models.RangeWithMetadata, []string) { + if len(ranges) == 0 { + return nil, nil, nil + } + + r, un, sR := GitVersionsToCommits(ranges, repos, metrics, cache) + if len(r) > 0 { + metrics.ResolvedRangesCount += len(r) + metrics.SetOutcome(models.Successful) + } + + if len(un) > 0 { + metrics.UnresolvedRangesCount += len(un) + if len(r) == 0 { + metrics.SetOutcome(models.NoCommitRanges) + } + } + + metrics.VersionSources = append(metrics.VersionSources, source) + + return r, un, sR +} diff --git a/vulnfeeds/conversion/grouping.go b/vulnfeeds/conversion/grouping.go index f8998e6beb2..d3983aa5f88 100644 --- a/vulnfeeds/conversion/grouping.go +++ b/vulnfeeds/conversion/grouping.go @@ -79,12 +79,13 @@ func GroupRanges(ranges []*osvschema.Range) []*osvschema.Range { } // Reconstruct ranges from groups - var newRanges []*osvschema.Range + newRanges := make([]*osvschema.Range, 0, len(order)) for _, key := range order { r := groups[key] r.Events = cleanEvents(r.GetEvents()) newRanges = append(newRanges, r) } + return newRanges } diff --git a/vulnfeeds/conversion/nvd/converter.go b/vulnfeeds/conversion/nvd/converter.go index f798f0384f5..58a92a4f7db 100644 --- a/vulnfeeds/conversion/nvd/converter.go +++ b/vulnfeeds/conversion/nvd/converter.go @@ -15,11 +15,9 @@ import ( "github.com/google/osv/vulnfeeds/cves" "github.com/google/osv/vulnfeeds/git" "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/utility" "github.com/google/osv/vulnfeeds/utility/logger" "github.com/google/osv/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" - "google.golang.org/protobuf/types/known/structpb" ) var ErrNoRanges = errors.New("no ranges") @@ -69,7 +67,7 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc metrics.SetOutcome(models.NoRepos) metrics.UnresolvedRangesCount += len(cpeRanges) - unresolvedDatabaseSpecificField := createUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) + unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) if unresolvedDatabaseSpecificField != nil { v.DatabaseSpecific = unresolvedDatabaseSpecificField } @@ -81,7 +79,7 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc } // If we have ranges, try to resolve them - r, un, sR := processRanges(cpeRanges, repos, metrics, cache, models.VersionSourceCPE) + r, un, sR := conversion.ProcessRanges(cpeRanges, repos, metrics, cache, models.VersionSourceCPE) if metrics.Outcome == models.Error { return models.Error } @@ -111,7 +109,7 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc if len(textRanges) > 0 { metrics.AddNote("Extracted versions from description: %v", textRanges) } - r, un, sR := processRanges(textRanges, repos, metrics, cache, models.VersionSourceDescription) + r, un, sR := conversion.ProcessRanges(textRanges, repos, metrics, cache, models.VersionSourceDescription) if metrics.Outcome == models.Error { return models.Error } @@ -132,18 +130,18 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc groupedRanges := conversion.GroupRanges(resolvedRanges) affected := MergeRangesAndCreateAffected(groupedRanges, commits, keys, metrics) v.Affected = append(v.Affected, affected) - + if metrics.Outcome == models.Error || (!outputMetrics && rejectFailed && metrics.Outcome != models.Successful) { return metrics.Outcome } - unresolvedDatabaseSpecificField := createUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) - // TODO: this should be if v.DatabaseSpecific != nil, initalise, otherwise add it. + unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) + // TODO: this should be if v.DatabaseSpecific != nil, initialise, otherwise add it. if unresolvedDatabaseSpecificField != nil { - v.DatabaseSpecific = unresolvedDatabaseSpecificField + if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedDatabaseSpecificField); err != nil { + logger.Warn("failed to add unresolved ranges to database specific: %v", err) + } } - - outputFiles(v, directory, maybeVendorName, maybeProductName, metrics, rejectFailed, outputMetrics) return metrics.Outcome @@ -496,51 +494,3 @@ func outputFiles(v *vulns.Vulnerability, dir string, vendor string, product stri metricsFile.Close() } } - -// processRanges attempts to resolve the given ranges to commits and updates the metrics accordingly. -func processRanges(ranges []models.RangeWithMetadata, repos []string, metrics *models.ConversionMetrics, cache *git.RepoTagsCache, source models.VersionSource) ([]*osvschema.Range, []models.RangeWithMetadata, []string) { - if len(ranges) == 0 { - return nil, nil, nil - } - - r, un, sR := conversion.GitVersionsToCommits(ranges, repos, metrics, cache) - if len(r) > 0 { - metrics.ResolvedRangesCount += len(r) - metrics.SetOutcome(models.Successful) - } - - if len(un) > 0 { - metrics.UnresolvedRangesCount += len(un) - if len(r) == 0 { - metrics.SetOutcome(models.NoCommitRanges) - } - } - - metrics.VersionSources = append(metrics.VersionSources, source) - - return r, un, sR -} - -func createUnresolvedDatabaseSpecificField(unresolvedRanges []models.RangeWithMetadata, metrics *models.ConversionMetrics) *structpb.Struct { - if len(unresolvedRanges) > 0 { - var unresolvedRangesMap []map[string]any - for _, ur := range unresolvedRanges { - urMap := map[string]any{ - "range": ur.Range, - "metadata": map[string]any{ - "cpe": ur.Metadata.CPE, - }, - } - unresolvedRangesMap = append(unresolvedRangesMap, urMap) - } - databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{ - "unresolved_ranges": unresolvedRangesMap, - }) - if err != nil { - metrics.AddNote("failed to make database specific: %v", err) - } - return databaseSpecific - } - - return nil -} diff --git a/vulnfeeds/cvelist2osv/default_extractor.go b/vulnfeeds/cvelist2osv/default_extractor.go index 9d52f068945..27c84fd72fe 100644 --- a/vulnfeeds/cvelist2osv/default_extractor.go +++ b/vulnfeeds/cvelist2osv/default_extractor.go @@ -5,7 +5,6 @@ import ( "github.com/google/osv/vulnfeeds/cves" "github.com/google/osv/vulnfeeds/git" "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/utility" "github.com/google/osv/vulnfeeds/utility/logger" "github.com/google/osv/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" @@ -37,14 +36,22 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln ranges := d.handleAffected(cve.Containers.CNA.Affected, metrics) + var unresolvedRanges []models.RangeWithMetadata if len(ranges) != 0 { - resolvedRanges, unresolvedRanges, _ := conversion.GitVersionsToCommits(ranges, repos, metrics, repoTagsCache) - if len(resolvedRanges) == 0 { + var nr []models.RangeWithMetadata + for _, r := range ranges { + nr = append(nr, models.RangeWithMetadata{ + Range: r, + }) + } + r, uR, _ := conversion.GitVersionsToCommits(nr, repos, metrics, repoTagsCache) + if len(r) == 0 { metrics.AddNote("Failed to convert git versions to commits") } else { gotVersions = true } - addRangesToAffected(resolvedRanges, unresolvedRanges, v, metrics) + addRangesToAffected(r, v, metrics) + unresolvedRanges = append(unresolvedRanges, uR...) } if !gotVersions { @@ -52,26 +59,43 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln versionRanges, _ := cpeVersionExtraction(cve, metrics) if len(versionRanges) != 0 { - resolvedRanges, unresolvedRanges, _ := conversion.GitVersionsToCommits(versionRanges, repos, metrics, repoTagsCache) - if len(resolvedRanges) == 0 { + var nr []models.RangeWithMetadata + for _, r := range versionRanges { + nr = append(nr, models.RangeWithMetadata{ + Range: r, + }) + } + r, uR, _ := conversion.GitVersionsToCommits(nr, repos, metrics, repoTagsCache) + if len(r) == 0 { metrics.AddNote("Failed to convert git versions to commits") } else { gotVersions = true } - addRangesToAffected(resolvedRanges, unresolvedRanges, v, metrics) + addRangesToAffected(r, v, metrics) + unresolvedRanges = append(unresolvedRanges, uR...) } } if !gotVersions { metrics.AddNote("No versions in CPEs so attempting extraction from description") - versionRanges := textVersionExtraction(cve, metrics) - if len(versionRanges) != 0 { - resolvedRanges, unresolvedRanges, _ := conversion.GitVersionsToCommits(versionRanges, repos, metrics, repoTagsCache) - if len(resolvedRanges) == 0 { + textRanges := cves.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics) + if len(textRanges) > 0 { + metrics.AddNote("Extracted versions from description: %v", textRanges) + } + if len(textRanges) != 0 { + r, uR, _ := conversion.GitVersionsToCommits(textRanges, repos, metrics, repoTagsCache) + if len(r) == 0 { metrics.AddNote("Failed to convert git versions to commits") } - addRangesToAffected(resolvedRanges, unresolvedRanges, v, metrics) + unresolvedRanges = append(unresolvedRanges, uR...) + } + } + + if len(unresolvedRanges) > 0 { + unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) + if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedDatabaseSpecificField); err != nil { + logger.Warn("failed to make database specific: %v", err) } } } @@ -135,19 +159,11 @@ func (d *DefaultVersionExtractor) FindNormalAffectedRanges(affected models.Affec return versionRanges, mostFrequentVersionType } -func addRangesToAffected(resolvedRanges []*osvschema.Range, unresolvedRanges []*osvschema.Range, v *vulns.Vulnerability, metrics *models.ConversionMetrics) { +func addRangesToAffected(resolvedRanges []*osvschema.Range, v *vulns.Vulnerability, metrics *models.ConversionMetrics) { if len(resolvedRanges) > 0 { aff := &osvschema.Affected{ Ranges: resolvedRanges, } - if len(unresolvedRanges) > 0 { - databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{"unresolved_ranges": unresolvedRanges}) - if err != nil { - logger.Warn("failed to make database specific: %v", err) - } else { - aff.DatabaseSpecific = databaseSpecific - } - } conversion.AddAffected(v, aff, metrics) } } diff --git a/vulnfeeds/cvelist2osv/strategies.go b/vulnfeeds/cvelist2osv/strategies.go index 38f0824e82b..23e26b60c33 100644 --- a/vulnfeeds/cvelist2osv/strategies.go +++ b/vulnfeeds/cvelist2osv/strategies.go @@ -2,7 +2,6 @@ package cvelist2osv import ( "github.com/google/osv/vulnfeeds/conversion" - "github.com/google/osv/vulnfeeds/cves" "github.com/google/osv/vulnfeeds/models" "github.com/google/osv/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" @@ -22,19 +21,6 @@ func cpeVersionExtraction(cve models.CVE5, metrics *models.ConversionMetrics) ([ return nil, err } -// textVersionExtraction is a helper function for CPE and description extraction. -func textVersionExtraction(cve models.CVE5, metrics *models.ConversionMetrics) []*osvschema.Range { - // As a last resort, try extracting versions from the description text. - versions := cves.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics) - if len(versions) > 0 { - // NOTE: These versions are not currently saved due to the need for better validation. - metrics.VersionSources = append(metrics.VersionSources, models.VersionSourceDescription) - metrics.AddNote("Extracted versions from description but did not save them: %+v", versions) - } - - return []*osvschema.Range{} -} - // initialNormalExtraction handles an expected case of version ranges in the affected field of CVE5 func initialNormalExtraction(vers models.Versions, metrics *models.ConversionMetrics, versionTypesCount map[VersionRangeType]int) ([]*osvschema.Range, VersionRangeType, bool) { if vers.Status != "affected" { diff --git a/vulnfeeds/git/repository.go b/vulnfeeds/git/repository.go index d4fa09050ab..f98ff9796cb 100644 --- a/vulnfeeds/git/repository.go +++ b/vulnfeeds/git/repository.go @@ -73,9 +73,10 @@ type RepoTagsMap struct { type RepoTagsCache struct { sync.RWMutex - m map[string]RepoTagsMap - invalid map[string]bool - canonicalLink map[string]string} + m map[string]RepoTagsMap + invalid map[string]bool + canonicalLink map[string]string +} func (c *RepoTagsCache) Get(repo string) (RepoTagsMap, bool) { c.RLock() @@ -132,6 +133,7 @@ func (c *RepoTagsCache) GetCanonicalLink(repo string) (string, bool) { return "", false } canonicalLink, ok := c.canonicalLink[repo] + return canonicalLink, ok } diff --git a/vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_functioning_link.yaml b/vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_functioning_link.yaml new file mode 100644 index 00000000000..bd4bb3c6a9e --- /dev/null +++ b/vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_functioning_link.yaml @@ -0,0 +1,51 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: git.kernel.org + form: + id: + - ee1fee900537b5d9560e9f937402de5ddc8412f3 + headers: + Accept: + - text/html + url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ee1fee900537b5d9560e9f937402de5ddc8412f3 + method: HEAD + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: "" + headers: + Content-Security-Policy: + - 'default-src ''self''; worker-src ''self'' blob:; style-src ''self'' ''unsafe-inline''; script-src-attr ''unsafe-hashes'' ''sha256-rQQdnklrOmulrf5mQ2YjUK7CGbu4ywAi21E8nGlJcDc''; img-src https:' + Content-Type: + - text/html; charset=UTF-8 + Date: + - Sun, 15 Mar 2026 23:51:42 GMT + Expires: + - Mon, 16 Mar 2026 00:51:42 GMT + Last-Modified: + - Sun, 15 Mar 2026 23:51:42 GMT + Referrer-Policy: + - same-origin + Server: + - nginx + Strict-Transport-Security: + - max-age=15768001 + Vary: + - Accept-Encoding + - Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: 200 OK + code: 200 + duration: 440.799062ms diff --git a/vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_link_that_404_s.yaml b/vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_link_that_404_s.yaml new file mode 100644 index 00000000000..9fe78ef7f9b --- /dev/null +++ b/vulnfeeds/git/testdata/TestValidateAndCanonicalizeLink_A_link_that_404_s.yaml @@ -0,0 +1,55 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: github.com + headers: + Accept: + - text/html + url: https://github.com/WebKit/webkit/commit/6f9b511a115311b13c06eb58038ddc2c78da5531 + method: HEAD + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: "" + headers: + Cache-Control: + - no-cache + Content-Security-Policy: + - 'default-src ''none''; base-uri ''self''; child-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/; connect-src ''self'' uploads.github.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com *.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com github.githubassets.com objects-origin.githubusercontent.com copilot-proxy.githubusercontent.com proxy.individual.githubcopilot.com proxy.business.githubcopilot.com proxy.enterprise.githubcopilot.com *.actions.githubusercontent.com wss://*.actions.githubusercontent.com productionresultssa0.blob.core.windows.net productionresultssa1.blob.core.windows.net productionresultssa2.blob.core.windows.net productionresultssa3.blob.core.windows.net productionresultssa4.blob.core.windows.net productionresultssa5.blob.core.windows.net productionresultssa6.blob.core.windows.net productionresultssa7.blob.core.windows.net productionresultssa8.blob.core.windows.net productionresultssa9.blob.core.windows.net productionresultssa10.blob.core.windows.net productionresultssa11.blob.core.windows.net productionresultssa12.blob.core.windows.net productionresultssa13.blob.core.windows.net productionresultssa14.blob.core.windows.net productionresultssa15.blob.core.windows.net productionresultssa16.blob.core.windows.net productionresultssa17.blob.core.windows.net productionresultssa18.blob.core.windows.net productionresultssa19.blob.core.windows.net github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com insights.github.com wss://alive.github.com wss://alive-staging.github.com api.githubcopilot.com api.individual.githubcopilot.com api.business.githubcopilot.com api.enterprise.githubcopilot.com; font-src github.githubassets.com; form-action ''self'' github.com gist.github.com copilot-workspace.githubnext.com objects-origin.githubusercontent.com; frame-ancestors ''none''; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; img-src ''self'' data: blob: github.githubassets.com media.githubusercontent.com camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com private-avatars.githubusercontent.com github-cloud.s3.amazonaws.com objects.githubusercontent.com release-assets.githubusercontent.com secured-user-images.githubusercontent.com user-images.githubusercontent.com private-user-images.githubusercontent.com opengraph.githubassets.com marketplace-screenshots.githubusercontent.com copilotprodattachments.blob.core.windows.net/github-production-copilot-attachments/ github-production-user-asset-6210df.s3.amazonaws.com customer-stories-feed.github.com spotlights-feed.github.com objects-origin.githubusercontent.com *.githubusercontent.com; manifest-src ''self''; media-src github.com user-images.githubusercontent.com secured-user-images.githubusercontent.com private-user-images.githubusercontent.com github-production-user-asset-6210df.s3.amazonaws.com gist.github.com github.githubassets.com; script-src github.githubassets.com; style-src ''unsafe-inline'' github.githubassets.com; upgrade-insecure-requests; worker-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/' + Content-Type: + - text/html; charset=utf-8 + Date: + - Sun, 15 Mar 2026 23:51:41 GMT + Referrer-Policy: + - no-referrer-when-downgrade + Server: + - github.com + Set-Cookie: + - _gh_sess=HMrlxmc%2Fb%2F1TmmGqTQzWSF6K3PP8zqktQBXZryX8Z5fUFdRTXVvE0HAr9K61HFAs0Avvqp3zzT011ncglKH2fELTFRYSKjKgzuPDtZU3PO3%2BzsvD2LxKCnX%2FvrVOaIs1lbxGUbnSmf5rSabJCI6vkY2Qz01W3M9EzcbcgAyh9XW1BSofuifKKvu1CWtMu34WcwVPY6s%2FFELIVc13re%2F1PNMOoVOW%2BrxCBXFRca7ALd%2F8Wjm7iWcqa%2BMzkwuokGG8CTK0yGyAhaLpWdzXCsNfGw%3D%3D--9mQ7%2FWdBKkFw5Tv0--zgdIuyAjFrh6sdywVP7OZg%3D%3D; path=/; HttpOnly; secure; SameSite=Lax + - _octo=GH1.1.1494558448.1773618700; expires=Mon, 15 Mar 2027 23:51:40 GMT; domain=.github.com; path=/; secure; SameSite=Lax + - logged_in=no; expires=Mon, 15 Mar 2027 23:51:40 GMT; domain=.github.com; path=/; HttpOnly; secure; SameSite=Lax + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, X-Requested-With, Sec-Fetch-Site,Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Request-Id: + - B0DB:1A1B7B:2753EB:306577:69B7460C + X-Repository-Download: + - git clone https://github.com/WebKit/WebKit.git + X-Xss-Protection: + - "0" + status: 404 Not Found + code: 404 + duration: 814.426424ms diff --git a/vulnfeeds/git/versions.go b/vulnfeeds/git/versions.go index f23a2b27d11..47667710551 100644 --- a/vulnfeeds/git/versions.go +++ b/vulnfeeds/git/versions.go @@ -245,5 +245,6 @@ func FindCanonicalLink(link string, httpClient *http.Client, cache *RepoTagsCach return link, err } cache.SetCanonicalLink(link, canonicalLink) + return canonicalLink, nil -} \ No newline at end of file +} diff --git a/vulnfeeds/git/versions_test.go b/vulnfeeds/git/versions_test.go index 07c8ae20962..36859ede63a 100644 --- a/vulnfeeds/git/versions_test.go +++ b/vulnfeeds/git/versions_test.go @@ -393,4 +393,4 @@ func TestValidateAndCanonicalizeLink_429(t *testing.T) { if err == nil { t.Errorf("ValidateAndCanonicalizeLink() expected error, got nil") } -} \ No newline at end of file +} diff --git a/vulnfeeds/models/types.go b/vulnfeeds/models/types.go index 27bb36055ff..c2cdd6e03c4 100644 --- a/vulnfeeds/models/types.go +++ b/vulnfeeds/models/types.go @@ -32,7 +32,7 @@ func SetCommitByType(ac *AffectedCommit, commitType CommitType, commitHash strin } type RangeWithMetadata struct { - Range *osvschema.Range + Range *osvschema.Range Metadata Metadata } @@ -40,7 +40,6 @@ type Metadata struct { CPE string } - func (ac *AffectedCommit) SetRepo(repo string) { // GitHub.com repos are demonstrably case-insensitive, and frequently // expressed in URLs with varying cases, so normalize them to lowercase. diff --git a/vulnfeeds/utility/utility.go b/vulnfeeds/utility/utility.go index 8e0db1c8e5d..5552b402477 100644 --- a/vulnfeeds/utility/utility.go +++ b/vulnfeeds/utility/utility.go @@ -79,6 +79,7 @@ func newStructpbValue(v any) (*structpb.Value, error) { if err != nil { return nil, fmt.Errorf("failed to convert proto message: %w", err) } + return structpb.NewValue(val) } @@ -101,8 +102,10 @@ func newStructpbValue(v any) (*structpb.Value, error) { if err != nil { return nil, err } + return structpb.NewStructValue(structpbMap), nil } + return structpb.NewValue(v) default: return structpb.NewValue(v) @@ -114,7 +117,7 @@ func newStructpbValue(v any) (*structpb.Value, error) { // It iterates through the list, converting any proto.Message elements into // any type via JSON marshalling, while other elements are included as-is. func structpbValueFromList(list []any) (*structpb.Value, error) { - var values []*structpb.Value + values := make([]*structpb.Value, 0, len(list)) for i, v := range list { val, err := newStructpbValue(v) if err != nil { From b2181b3066113291173f1aeca3bb216e234ee621 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 16 Mar 2026 00:40:48 +0000 Subject: [PATCH 4/9] add database specific --- vulnfeeds/conversion/nvd/converter.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vulnfeeds/conversion/nvd/converter.go b/vulnfeeds/conversion/nvd/converter.go index 58a92a4f7db..be46023e6aa 100644 --- a/vulnfeeds/conversion/nvd/converter.go +++ b/vulnfeeds/conversion/nvd/converter.go @@ -15,6 +15,7 @@ import ( "github.com/google/osv/vulnfeeds/cves" "github.com/google/osv/vulnfeeds/git" "github.com/google/osv/vulnfeeds/models" + "github.com/google/osv/vulnfeeds/utility" "github.com/google/osv/vulnfeeds/utility/logger" "github.com/google/osv/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" @@ -45,6 +46,12 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc // Create basic OSV record v := vulns.FromNVDCVE(cve.ID, cve) + databaseSpecific, err := utility.NewStructpbFromMap(make(map[string]any)) + if err != nil { + metrics.AddNote("Failed to convert database specific: %v", err) + } else { + v.DatabaseSpecific = databaseSpecific + } // At the bare minimum, we want to attempt to extract the raw version information // from CPEs, whether or not they can resolve to commits. From 0eecd4e41a999958841d4e960c531b9d1e5f7d30 Mon Sep 17 00:00:00 2001 From: Jess Lowe <86962800+jess-lowe@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:52:50 +1100 Subject: [PATCH 5/9] fix: introduced isn't required This was causing records whose introduced tag doesn't resolve but their fixed tag resolving to just give unresolved ranges instead of setting introduced to 0 --- vulnfeeds/conversion/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnfeeds/conversion/common.go b/vulnfeeds/conversion/common.go index d376a6bce18..feb54dbcc8f 100644 --- a/vulnfeeds/conversion/common.go +++ b/vulnfeeds/conversion/common.go @@ -246,7 +246,7 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri metrics.AddNote("error resolving version to commit - %s - %s", lastAffected, err) } - if introducedCommit != "" && (fixedCommit != "" || lastAffectedCommit != "") { + if fixedCommit != "" || lastAffectedCommit != "" { var newVR *osvschema.Range if fixedCommit != "" { From 404775f130b0680fdfeea26b9dc69e7422cadb0d Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 16 Mar 2026 02:33:29 +0000 Subject: [PATCH 6/9] Move MergeRangesAndCreateAffected to common --- vulnfeeds/conversion/grouping.go | 117 ++++++++++++++++++++++++++ vulnfeeds/conversion/nvd/converter.go | 116 +------------------------ 2 files changed, 118 insertions(+), 115 deletions(-) diff --git a/vulnfeeds/conversion/grouping.go b/vulnfeeds/conversion/grouping.go index d3983aa5f88..fc20eae3d66 100644 --- a/vulnfeeds/conversion/grouping.go +++ b/vulnfeeds/conversion/grouping.go @@ -5,6 +5,7 @@ import ( "log/slog" "slices" + "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" @@ -249,3 +250,119 @@ func cleanEvents(events []*osvschema.Event) []*osvschema.Event { return finalEvents } + +// MergeRangesAndCreateAffected combines resolved and unresolved ranges with commits to create an OSV Affected object. +// It merges ranges for the same repository and adds commit events to the appropriate ranges at the end. +// +// Arguments: +// - resolvedRanges: A slice of resolved OSV ranges to be merged. +// - commits: A slice of affected commits to be converted into events and added to ranges. +// - successfulRepos: A slice of repository URLs that were successfully processed. +// - metrics: A pointer to ConversionMetrics to track the outcome and notes. +func MergeRangesAndCreateAffected(resolvedRanges []*osvschema.Range, commits []models.AffectedCommit, successfulRepos []string, metrics *models.ConversionMetrics) *osvschema.Affected { + var newResolvedRanges []*osvschema.Range + // Combine the ranges appropriately + if len(resolvedRanges) > 0 { + slices.Sort(successfulRepos) + successfulRepos = slices.Compact(successfulRepos) + for _, repo := range successfulRepos { + var mergedRange *osvschema.Range + for _, vr := range resolvedRanges { + if vr.GetRepo() == repo { + if mergedRange == nil { + mergedRange = vr + } else { + var err error + mergedRange, err = MergeTwoRanges(mergedRange, vr) + if err != nil { + metrics.AddNote("Failed to merge ranges: %v", err) + } + } + } + } + if len(commits) > 0 { + for _, commit := range commits { + if commit.Repo == repo { + if mergedRange == nil { + mergedRange = BuildVersionRange(commit.Introduced, commit.LastAffected, commit.Fixed) + mergedRange.Repo = repo + } else { + event := convertCommitToEvent(commit) + if event != nil { + addEventToRange(mergedRange, event) + } + } + } + } + } + if mergedRange != nil { + newResolvedRanges = append(newResolvedRanges, mergedRange) + } + } + } + + // if there are no resolved version but there are commits, we should create a range for each commit + if len(resolvedRanges) == 0 && len(commits) > 0 { + for _, commit := range commits { + newResolvedRanges = append(newResolvedRanges, BuildVersionRange(commit.Introduced, commit.LastAffected, commit.Fixed)) + metrics.ResolvedRangesCount++ + } + } + + newAffected := &osvschema.Affected{ + Ranges: newResolvedRanges, + } + + return newAffected +} + +// addEventToRange adds an event to a version range, avoiding duplicates. +// Introduced events are prepended to the events list, while others are appended. +// +// Arguments: +// - versionRange: The OSV range to which the event will be added. +// - event: The OSV event (Introduced, Fixed, or LastAffected) to add. +func addEventToRange(versionRange *osvschema.Range, event *osvschema.Event) { + // Handle duplicate events being added + for _, e := range versionRange.GetEvents() { + if e.GetIntroduced() != "" && e.GetIntroduced() == event.GetIntroduced() { + return + } + if e.GetFixed() != "" && e.GetFixed() == event.GetFixed() { + return + } + if e.GetLastAffected() != "" && e.GetLastAffected() == event.GetLastAffected() { + return + } + } + //TODO: maybe handle if the fixed event appears as an introduced event or similar. + + if event.GetIntroduced() != "" { + versionRange.Events = append([]*osvschema.Event{{ + Introduced: event.GetIntroduced()}}, versionRange.GetEvents()...) + } else { + versionRange.Events = append(versionRange.Events, event) + } +} + +// convertCommitToEvent creates an OSV Event from an AffectedCommit. +// It returns an event with the Introduced, Fixed, or LastAffected value from the commit. +func convertCommitToEvent(commit models.AffectedCommit) *osvschema.Event { + if commit.Introduced != "" { + return &osvschema.Event{ + Introduced: commit.Introduced, + } + } + if commit.Fixed != "" { + return &osvschema.Event{ + Fixed: commit.Fixed, + } + } + if commit.LastAffected != "" { + return &osvschema.Event{ + LastAffected: commit.LastAffected, + } + } + + return nil +} \ No newline at end of file diff --git a/vulnfeeds/conversion/nvd/converter.go b/vulnfeeds/conversion/nvd/converter.go index be46023e6aa..53031a9b30a 100644 --- a/vulnfeeds/conversion/nvd/converter.go +++ b/vulnfeeds/conversion/nvd/converter.go @@ -135,7 +135,7 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc // Use the successful repos for more efficient merging. keys := slices.Collect(maps.Keys(successfulRepos)) groupedRanges := conversion.GroupRanges(resolvedRanges) - affected := MergeRangesAndCreateAffected(groupedRanges, commits, keys, metrics) + affected := conversion.MergeRangesAndCreateAffected(groupedRanges, commits, keys, metrics) v.Affected = append(v.Affected, affected) if metrics.Outcome == models.Error || (!outputMetrics && rejectFailed && metrics.Outcome != models.Successful) { @@ -340,121 +340,7 @@ func FindRepos(cve models.NVDCVE, vpRepoCache *cves.VPRepoCache, repoTagsCache * return reposForCVE } -// MergeRangesAndCreateAffected combines resolved and unresolved ranges with commits to create an OSV Affected object. -// It merges ranges for the same repository and adds commit events to the appropriate ranges at the end. -// -// Arguments: -// - resolvedRanges: A slice of resolved OSV ranges to be merged. -// - commits: A slice of affected commits to be converted into events and added to ranges. -// - successfulRepos: A slice of repository URLs that were successfully processed. -// - metrics: A pointer to ConversionMetrics to track the outcome and notes. -func MergeRangesAndCreateAffected(resolvedRanges []*osvschema.Range, commits []models.AffectedCommit, successfulRepos []string, metrics *models.ConversionMetrics) *osvschema.Affected { - var newResolvedRanges []*osvschema.Range - // Combine the ranges appropriately - if len(resolvedRanges) > 0 { - slices.Sort(successfulRepos) - successfulRepos = slices.Compact(successfulRepos) - for _, repo := range successfulRepos { - var mergedRange *osvschema.Range - for _, vr := range resolvedRanges { - if vr.GetRepo() == repo { - if mergedRange == nil { - mergedRange = vr - } else { - var err error - mergedRange, err = conversion.MergeTwoRanges(mergedRange, vr) - if err != nil { - metrics.AddNote("Failed to merge ranges: %v", err) - } - } - } - } - if len(commits) > 0 { - for _, commit := range commits { - if commit.Repo == repo { - if mergedRange == nil { - mergedRange = conversion.BuildVersionRange(commit.Introduced, commit.LastAffected, commit.Fixed) - mergedRange.Repo = repo - } else { - event := convertCommitToEvent(commit) - if event != nil { - addEventToRange(mergedRange, event) - } - } - } - } - } - if mergedRange != nil { - newResolvedRanges = append(newResolvedRanges, mergedRange) - } - } - } - - // if there are no resolved version but there are commits, we should create a range for each commit - if len(resolvedRanges) == 0 && len(commits) > 0 { - for _, commit := range commits { - newResolvedRanges = append(newResolvedRanges, conversion.BuildVersionRange(commit.Introduced, commit.LastAffected, commit.Fixed)) - metrics.ResolvedRangesCount++ - } - } - - newAffected := &osvschema.Affected{ - Ranges: newResolvedRanges, - } - - return newAffected -} - -// addEventToRange adds an event to a version range, avoiding duplicates. -// Introduced events are prepended to the events list, while others are appended. -// -// Arguments: -// - versionRange: The OSV range to which the event will be added. -// - event: The OSV event (Introduced, Fixed, or LastAffected) to add. -func addEventToRange(versionRange *osvschema.Range, event *osvschema.Event) { - // Handle duplicate events being added - for _, e := range versionRange.GetEvents() { - if e.GetIntroduced() != "" && e.GetIntroduced() == event.GetIntroduced() { - return - } - if e.GetFixed() != "" && e.GetFixed() == event.GetFixed() { - return - } - if e.GetLastAffected() != "" && e.GetLastAffected() == event.GetLastAffected() { - return - } - } - //TODO: maybe handle if the fixed event appears as an introduced event or similar. - - if event.GetIntroduced() != "" { - versionRange.Events = append([]*osvschema.Event{{ - Introduced: event.GetIntroduced()}}, versionRange.GetEvents()...) - } else { - versionRange.Events = append(versionRange.Events, event) - } -} -// convertCommitToEvent creates an OSV Event from an AffectedCommit. -// It returns an event with the Introduced, Fixed, or LastAffected value from the commit. -func convertCommitToEvent(commit models.AffectedCommit) *osvschema.Event { - if commit.Introduced != "" { - return &osvschema.Event{ - Introduced: commit.Introduced, - } - } - if commit.Fixed != "" { - return &osvschema.Event{ - Fixed: commit.Fixed, - } - } - if commit.LastAffected != "" { - return &osvschema.Event{ - LastAffected: commit.LastAffected, - } - } - - return nil -} // outputFiles writes the OSV vulnerability record and conversion metrics to files in the specified directory. // It creates the necessary subdirectories based on the vendor and product names and handles whether or not From 9f1fbcbe0d5b4219ecbcc61e09668bcd85bb8303 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 16 Mar 2026 02:34:04 +0000 Subject: [PATCH 7/9] handle when introduced the same as lessthan or equal meaning its not a range --- vulnfeeds/cvelist2osv/strategies.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vulnfeeds/cvelist2osv/strategies.go b/vulnfeeds/cvelist2osv/strategies.go index 23e26b60c33..c51ac5aac93 100644 --- a/vulnfeeds/cvelist2osv/strategies.go +++ b/vulnfeeds/cvelist2osv/strategies.go @@ -41,13 +41,18 @@ func initialNormalExtraction(vers models.Versions, metrics *models.ConversionMet vLTOEQual := vulns.CheckQuality(vers.LessThanOrEqual) hasRange := vLessThanQual.AtLeast(acceptableQuality) || vLTOEQual.AtLeast(acceptableQuality) - metrics.AddNote("Range detected: %v", hasRange) + // Handle cases where 'lessThan' is mistakenly the same as 'version'. if vers.LessThan != "" && vers.LessThan == vers.Version { metrics.AddNote("Warning: lessThan (%s) is the same as introduced (%s)\n", vers.LessThan, vers.Version) hasRange = false } + if vers.LessThanOrEqual != "" && vers.LessThanOrEqual == vers.Version { + metrics.AddNote("Warning: lessThanOrEqual (%s) is the same as introduced (%s)\n", vers.LessThanOrEqual, vers.Version) + hasRange = false + } + metrics.AddNote("Range detected: %v", hasRange) if hasRange { if vQuality.AtLeast(acceptableQuality) { introduced = vers.Version From 53010bdcbd7e0a7b192ca08cd5fb86f30dba5974 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 16 Mar 2026 02:34:23 +0000 Subject: [PATCH 8/9] handle unresolved signatures --- vulnfeeds/cvelist2osv/default_extractor.go | 43 +++++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/vulnfeeds/cvelist2osv/default_extractor.go b/vulnfeeds/cvelist2osv/default_extractor.go index 27c84fd72fe..adc6c9c55ab 100644 --- a/vulnfeeds/cvelist2osv/default_extractor.go +++ b/vulnfeeds/cvelist2osv/default_extractor.go @@ -1,6 +1,9 @@ package cvelist2osv import ( + "maps" + "slices" + "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/cves" "github.com/google/osv/vulnfeeds/git" @@ -35,7 +38,8 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln repoTagsCache := &git.RepoTagsCache{} ranges := d.handleAffected(cve.Containers.CNA.Affected, metrics) - + successfulRepos := make(map[string]bool) + var resolvedRanges []*osvschema.Range var unresolvedRanges []models.RangeWithMetadata if len(ranges) != 0 { var nr []models.RangeWithMetadata @@ -44,14 +48,21 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln Range: r, }) } - r, uR, _ := conversion.GitVersionsToCommits(nr, repos, metrics, repoTagsCache) + // r, uR, successfulRepos := conversion.GitVersionsToCommits(nr, repos, metrics, repoTagsCache) + r, un, sR := conversion.ProcessRanges(nr, repos, metrics, repoTagsCache, models.VersionSourceAffected) + resolvedRanges = append(resolvedRanges, r...) + unresolvedRanges = append(unresolvedRanges, un...) + for _, s := range sR { + successfulRepos[s] = true + } if len(r) == 0 { metrics.AddNote("Failed to convert git versions to commits") } else { gotVersions = true + metrics.SetOutcome(models.Successful) } - addRangesToAffected(r, v, metrics) - unresolvedRanges = append(unresolvedRanges, uR...) + + unresolvedRanges = append(unresolvedRanges, un...) } if !gotVersions { @@ -65,14 +76,17 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln Range: r, }) } - r, uR, _ := conversion.GitVersionsToCommits(nr, repos, metrics, repoTagsCache) + r, un, sR := conversion.ProcessRanges(nr, repos, metrics, repoTagsCache, models.VersionSourceAffected) + resolvedRanges = append(resolvedRanges, r...) + unresolvedRanges = append(unresolvedRanges, un...) + for _, s := range sR { + successfulRepos[s] = true + } if len(r) == 0 { metrics.AddNote("Failed to convert git versions to commits") } else { gotVersions = true } - addRangesToAffected(r, v, metrics) - unresolvedRanges = append(unresolvedRanges, uR...) } } @@ -83,15 +97,24 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln metrics.AddNote("Extracted versions from description: %v", textRanges) } if len(textRanges) != 0 { - r, uR, _ := conversion.GitVersionsToCommits(textRanges, repos, metrics, repoTagsCache) + r, un, sR := conversion.ProcessRanges(textRanges, repos, metrics, repoTagsCache, models.VersionSourceAffected) + resolvedRanges = append(resolvedRanges, r...) + unresolvedRanges = append(unresolvedRanges, un...) + for _, s := range sR { + successfulRepos[s] = true + } if len(r) == 0 { metrics.AddNote("Failed to convert git versions to commits") } - - unresolvedRanges = append(unresolvedRanges, uR...) } } + keys := slices.Collect(maps.Keys(successfulRepos)) + groupedRanges := conversion.GroupRanges(resolvedRanges) + affected := conversion.MergeRangesAndCreateAffected(groupedRanges, nil, keys, metrics) + v.Affected = append(v.Affected, affected) + + if len(unresolvedRanges) > 0 { unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedDatabaseSpecificField); err != nil { From af6cc224a999b2ba759d48e04620e230b26031f8 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 16 Mar 2026 22:44:00 +0000 Subject: [PATCH 9/9] fix nested unresolved ranges and duplicate unresolved ranges. --- vulnfeeds/conversion/common.go | 13 +++++++------ vulnfeeds/conversion/nvd/converter.go | 15 ++++++++------- .../cvelist2osv/__snapshots__/converter_test.snap | 3 +++ vulnfeeds/cvelist2osv/default_extractor.go | 6 ++---- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/vulnfeeds/conversion/common.go b/vulnfeeds/conversion/common.go index feb54dbcc8f..8328e2cdaa4 100644 --- a/vulnfeeds/conversion/common.go +++ b/vulnfeeds/conversion/common.go @@ -464,7 +464,7 @@ func deduplicateList(list []any) []any { return unique } -func CreateUnresolvedDatabaseSpecificField(unresolvedRanges []models.RangeWithMetadata, metrics *models.ConversionMetrics) *structpb.Struct { +func CreateUnresolvedRanges(unresolvedRanges []models.RangeWithMetadata) *structpb.ListValue { if len(unresolvedRanges) > 0 { var unresolvedRangesMap []map[string]any for _, ur := range unresolvedRanges { @@ -478,14 +478,15 @@ func CreateUnresolvedDatabaseSpecificField(unresolvedRanges []models.RangeWithMe } unresolvedRangesMap = append(unresolvedRangesMap, urMap) } - databaseSpecific, err := utility.NewStructpbFromMap(map[string]any{ - "unresolved_ranges": unresolvedRangesMap, + + ds, err := utility.NewStructpbFromMap(map[string]any{ + "list": unresolvedRangesMap, }) if err != nil { - metrics.AddNote("failed to make database specific: %v", err) + logger.Warn("failed to convert unresolved ranges to structpb", "err", err) + return nil } - - return databaseSpecific + return ds.Fields["list"].GetListValue() } return nil diff --git a/vulnfeeds/conversion/nvd/converter.go b/vulnfeeds/conversion/nvd/converter.go index 53031a9b30a..6bf82885b1a 100644 --- a/vulnfeeds/conversion/nvd/converter.go +++ b/vulnfeeds/conversion/nvd/converter.go @@ -74,9 +74,11 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc metrics.SetOutcome(models.NoRepos) metrics.UnresolvedRangesCount += len(cpeRanges) - unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) - if unresolvedDatabaseSpecificField != nil { - v.DatabaseSpecific = unresolvedDatabaseSpecificField + unresolvedRangesList := conversion.CreateUnresolvedRanges(unresolvedRanges) + if unresolvedRangesList != nil { + if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedRangesList); err != nil { + logger.Warn("failed to add unresolved ranges to database specific: %v", err) + } } // Exit early @@ -141,10 +143,9 @@ func CVEToOSV(cve models.NVDCVE, repos []string, cache *git.RepoTagsCache, direc if metrics.Outcome == models.Error || (!outputMetrics && rejectFailed && metrics.Outcome != models.Successful) { return metrics.Outcome } - unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) - // TODO: this should be if v.DatabaseSpecific != nil, initialise, otherwise add it. - if unresolvedDatabaseSpecificField != nil { - if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedDatabaseSpecificField); err != nil { + unresolvedRangesList := conversion.CreateUnresolvedRanges(unresolvedRanges) + if unresolvedRangesList != nil { + if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedRangesList); err != nil { logger.Warn("failed to add unresolved ranges to database specific: %v", err) } } diff --git a/vulnfeeds/cvelist2osv/__snapshots__/converter_test.snap b/vulnfeeds/cvelist2osv/__snapshots__/converter_test.snap index 079da532b0f..bd357f7441f 100755 --- a/vulnfeeds/cvelist2osv/__snapshots__/converter_test.snap +++ b/vulnfeeds/cvelist2osv/__snapshots__/converter_test.snap @@ -1,6 +1,9 @@ [TestConvertAndExportCVEToOSV/disputed_record - 1] { + "affected": [ + {} + ], "database_specific": { "cna_assigner": "unknown", "isDisputed": true, diff --git a/vulnfeeds/cvelist2osv/default_extractor.go b/vulnfeeds/cvelist2osv/default_extractor.go index adc6c9c55ab..b944ffdc18c 100644 --- a/vulnfeeds/cvelist2osv/default_extractor.go +++ b/vulnfeeds/cvelist2osv/default_extractor.go @@ -61,8 +61,6 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln gotVersions = true metrics.SetOutcome(models.Successful) } - - unresolvedRanges = append(unresolvedRanges, un...) } if !gotVersions { @@ -116,8 +114,8 @@ func (d *DefaultVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vuln if len(unresolvedRanges) > 0 { - unresolvedDatabaseSpecificField := conversion.CreateUnresolvedDatabaseSpecificField(unresolvedRanges, metrics) - if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedDatabaseSpecificField); err != nil { + unresolvedRangesList := conversion.CreateUnresolvedRanges(unresolvedRanges) + if err := conversion.AddFieldToDatabaseSpecific(v.DatabaseSpecific, "unresolved_ranges", unresolvedRangesList); err != nil { logger.Warn("failed to make database specific: %v", err) } }