From 2df0ef51192947bbfef25e6cd398177e2a1230e1 Mon Sep 17 00:00:00 2001 From: Harshit Jain Date: Wed, 24 Dec 2025 13:10:58 +0530 Subject: [PATCH 1/2] fix(vulnfeeds): add more phrases for extracting versions --- vulnfeeds/cves/versions.go | 85 ++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/vulnfeeds/cves/versions.go b/vulnfeeds/cves/versions.go index 163c9a229a3..f2f8e847b29 100644 --- a/vulnfeeds/cves/versions.go +++ b/vulnfeeds/cves/versions.go @@ -608,34 +608,59 @@ func processExtractedVersion(version string) string { } func ExtractVersionsFromText(validVersions []string, text string) ([]models.AffectedVersion, []string) { - // Match: + // Match patterns where version comes AFTER keyword: // - x.x.x before x.x.x // - x.x.x through x.x.x // - through x.x.x // - before x.x.x - pattern := regexp.MustCompile(`(?i)([\w.+\-]+)?\s+(through|before)\s+(?:version\s+)?([\w.+\-]+)`) - matches := pattern.FindAllStringSubmatch(text, -1) - if matches == nil { + // - x.x.x up to and including x.x.x + // - prior to x.x.x + // - below x.x.x + prefixPattern := regexp.MustCompile(`(?i)([\w.+\-]+)?\s+(through|before|up to and including|prior to|below)\s+(?:version\s+)?([\w.+\-]+)`) + + // Match patterns where version comes BEFORE keyword (trailing patterns): + // - x.x.x and earlier + // - x.x.x or older + // - x.x.x and below + trailingPattern := regexp.MustCompile(`(?i)(?:version\s+)?([\w.+\-]+)\s+(and earlier|or older|and below)`) + + prefixMatches := prefixPattern.FindAllStringSubmatch(text, -1) + trailingMatches := trailingPattern.FindAllStringSubmatch(text, -1) + + if prefixMatches == nil && trailingMatches == nil { return nil, []string{"Failed to parse versions from text"} } var notes []string - versions := make([]models.AffectedVersion, 0, len(matches)) + versions := make([]models.AffectedVersion, 0, len(prefixMatches)+len(trailingMatches)) + + // Keywords that imply the version is the last affected (inclusive), not fixed + inclusiveKeywords := map[string]bool{ + "through": true, + "up to and including": true, + } - for _, match := range matches { + // Process prefix pattern matches (version comes after keyword) + for _, match := range prefixMatches { // Trim periods that are part of sentences. introduced := processExtractedVersion(match[1]) fixed := processExtractedVersion(match[3]) lastaffected := "" - if match[2] == "through" { - // "Through" implies inclusive range, so the fixed version is the one that comes after. + keyword := strings.ToLower(match[2]) + + if inclusiveKeywords[keyword] { + // These keywords imply inclusive range, so the version is lastAffected. + // Try to find the next version as "fixed", otherwise use lastAffected. var err error - fixed, err = nextVersion(validVersions, fixed) + nextFixed, err := nextVersion(validVersions, fixed) if err != nil { notes = append(notes, err.Error()) // if that inference failed, we know this version was definitely still vulnerable. lastaffected = cleanVersion(match[3]) + fixed = "" notes = append(notes, fmt.Sprintf("Using %s as last_affected version instead", cleanVersion(match[3]))) + } else { + fixed = nextFixed } } @@ -665,6 +690,48 @@ func ExtractVersionsFromText(validVersions []string, text string) ([]models.Affe }) } + // Process trailing pattern matches (version comes before keyword like "x.x.x and earlier") + for _, match := range trailingMatches { + // For trailing patterns: match[1] is the version, match[2] is the keyword + versionStr := processExtractedVersion(match[1]) + lastaffected := "" + fixed := "" + + if versionStr == "" { + notes = append(notes, "Failed to match version from trailing pattern") + continue + } + + // Try to find the next version as "fixed", otherwise use lastAffected + nextFixed, err := nextVersion(validVersions, versionStr) + if err != nil { + notes = append(notes, err.Error()) + // if that inference failed, we know this version was definitely still vulnerable. + lastaffected = cleanVersion(match[1]) + notes = append(notes, fmt.Sprintf("Using %s as last_affected version instead", cleanVersion(match[1]))) + } else { + fixed = nextFixed + } + + if fixed == "" && lastaffected == "" { + notes = append(notes, "Failed to match version range from text") + continue + } + + if fixed != "" && !HasVersion(validVersions, fixed) { + notes = append(notes, fmt.Sprintf("Extracted fixed version %s is not a valid version", fixed)) + } + if lastaffected != "" && !HasVersion(validVersions, lastaffected) { + notes = append(notes, fmt.Sprintf("Extracted last_affected version %s is not a valid version", lastaffected)) + } + + versions = append(versions, models.AffectedVersion{ + Introduced: "", + Fixed: fixed, + LastAffected: lastaffected, + }) + } + return versions, notes } From e1567979f5d8f403d00259b33fc94d7d5d8cfd33 Mon Sep 17 00:00:00 2001 From: Harshit Jain Date: Tue, 30 Dec 2025 06:30:13 +0530 Subject: [PATCH 2/2] test(vulnfeeds): add real-world test cases for version extraction --- vulnfeeds/cves/versions_test.go | 97 +++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/vulnfeeds/cves/versions_test.go b/vulnfeeds/cves/versions_test.go index 69e9bd9411f..21c2a17641e 100644 --- a/vulnfeeds/cves/versions_test.go +++ b/vulnfeeds/cves/versions_test.go @@ -1612,3 +1612,100 @@ func TestBuildVersionRange(t *testing.T) { }) } } + +func TestExtractVersionsFromText(t *testing.T) { + tests := []struct { + name string + description string + validVersions []string + expectedVersions []models.AffectedVersion + }{ + { + name: "up to and including - CVE-2025-11411", + description: "NLnet Labs Unbound up to and including version 1.24.1 is vulnerable to possible domain hijack attacks.", + validVersions: []string{ + "1.24.0", "1.24.1", "1.24.2", + }, + expectedVersions: []models.AffectedVersion{ + { + // "up to and including" implies inclusive upper bound. + // Since validVersions has "1.24.2" as next, logic prefers Fixed="1.24.2" over LastAffected="1.24.1". + Fixed: "1.24.2", + }, + }, + }, + { + name: "prior to - CVE-2022-3306", + description: "The vulnerability affected versions prior to 106.0.5249.62 and was reported on April 27, 2022.", + validVersions: []string{}, + expectedVersions: []models.AffectedVersion{ + { + Fixed: "106.0.5249.62", + }, + }, + }, + { + name: "below - CVE-2024-29945", + description: "In Splunk Enterprise versions below 9.2.1, 9.1.4, and 9.0.9, authentication tokens can be exposed.", + validVersions: []string{}, + expectedVersions: []models.AffectedVersion{ + { + Fixed: "9.2.1", + }, + }, + }, + { + name: "and earlier - CVE-2025-27199", + description: "Adobe Animate versions 24.0.7 and earlier are affected by a Heap-based Buffer Overflow vulnerability.", + validVersions: []string{ + "23.0.0", "24.0.0", "24.0.7", "24.0.8", + }, + expectedVersions: []models.AffectedVersion{ + { + Fixed: "24.0.8", + }, + }, + }, + { + name: "and earlier - CVE-2025-27199 (No next version)", + description: "Adobe Animate versions 24.0.7 and earlier are affected by a Heap-based Buffer Overflow vulnerability.", + validVersions: []string{ + "24.0.7", + }, + expectedVersions: []models.AffectedVersion{ + { + LastAffected: "24.0.7", + }, + }, + }, + { + name: "or older - CVE-2025-0725", + description: "A vulnerability affects systems utilizing zlib version 1.2.0.3 or older.", + validVersions: []string{}, + expectedVersions: []models.AffectedVersion{ + { + LastAffected: "1.2.0.3", + }, + }, + }, + { + name: "and below - CVE-2025-54591", + description: "FreshRSS, in versions 1.26.3 and below, exposes information about feeds.", + validVersions: []string{}, + expectedVersions: []models.AffectedVersion{ + { + LastAffected: "1.26.3", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotVersions, _ := ExtractVersionsFromText(tc.validVersions, tc.description) + if diff := cmp.Diff(tc.expectedVersions, gotVersions); diff != "" { + t.Errorf("ExtractVersionsFromText() mismatch (-want +got):\n%s", diff) + } + }) + } +}