From 0efaf6d3b0757decffcdf6fb16ef56dfc69ebe57 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Wed, 15 Jul 2026 05:54:41 +0000 Subject: [PATCH 01/13] support wordpress ecosystem extraction using cve5 --- vulnfeeds/conversion/cve5/extraction.go | 2 + .../cve5/version_extraction_test.go | 32 +++- .../conversion/cve5/wordfence_extractor.go | 163 ++++++++++++++++++ 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 vulnfeeds/conversion/cve5/wordfence_extractor.go diff --git a/vulnfeeds/conversion/cve5/extraction.go b/vulnfeeds/conversion/cve5/extraction.go index ebd3491f483..599efb677f1 100644 --- a/vulnfeeds/conversion/cve5/extraction.go +++ b/vulnfeeds/conversion/cve5/extraction.go @@ -16,6 +16,8 @@ func GetVersionExtractor(cna string) VersionExtractor { switch cna { case "Linux": return &LinuxVersionExtractor{} + case "Wordfence": + return &WordfenceVersionExtractor{} default: return &DefaultVersionExtractor{} } diff --git a/vulnfeeds/conversion/cve5/version_extraction_test.go b/vulnfeeds/conversion/cve5/version_extraction_test.go index b3fc79fe8b4..575c0ece5c3 100644 --- a/vulnfeeds/conversion/cve5/version_extraction_test.go +++ b/vulnfeeds/conversion/cve5/version_extraction_test.go @@ -381,6 +381,15 @@ func TestGetVersionExtractor(t *testing.T) { }, expectedType: reflect.TypeOf(&LinuxVersionExtractor{}), }, + { + name: "Wordfence CVE", + cve: models.CVE5{ + Metadata: models.CVE5Metadata{ + AssignerShortName: "Wordfence", + }, + }, + expectedType: reflect.TypeOf(&WordfenceVersionExtractor{}), + }, { name: "Default CVE", cve: models.CVE5{ @@ -593,13 +602,34 @@ func TestExtractVersions(t *testing.T) { }, }}, }, + { + name: "CVE-2026-1293", + cve: loadTestData(t, "CVE-2026-1293"), + cnaAssigner: "Wordfence", + repos: []string{}, + expectedAffected: []*osvschema.Affected{{ + Package: &osvschema.Package{ + Ecosystem: "WordPress", + Name: "wordpress-seo", + }, + Ranges: []*osvschema.Range{{ + Type: osvschema.Range_ECOSYSTEM, + Events: []*osvschema.Event{ + {Introduced: "0"}, + {LastAffected: "26.8"}, + }, + }}, + }}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { metrics := &models.ConversionMetrics{} v := vulns.Vulnerability{ - Vulnerability: &osvschema.Vulnerability{}, + Vulnerability: &osvschema.Vulnerability{ + References: vulns.ClassifyReferences(identifyPossibleURLs(tc.cve)), + }, } extractor := GetVersionExtractor(tc.cnaAssigner) extractor.ExtractVersions(tc.cve, &v, metrics, tc.repos) diff --git a/vulnfeeds/conversion/cve5/wordfence_extractor.go b/vulnfeeds/conversion/cve5/wordfence_extractor.go new file mode 100644 index 00000000000..41940159f0c --- /dev/null +++ b/vulnfeeds/conversion/cve5/wordfence_extractor.go @@ -0,0 +1,163 @@ +package cve5 + +import ( + "regexp" + "strings" + + c "github.com/google/osv/vulnfeeds/conversion" + "github.com/google/osv/vulnfeeds/models" + "github.com/google/osv/vulnfeeds/vulns" + "github.com/ossf/osv-schema/bindings/go/osvconstants" + "github.com/ossf/osv-schema/bindings/go/osvschema" +) + +// WordfenceVersionExtractor provides version extraction logic for Wordfence CVEs. +type WordfenceVersionExtractor struct { + DefaultVersionExtractor +} + +var _ VersionExtractor = &WordfenceVersionExtractor{} + +var ( + tracRegex = regexp.MustCompile(`plugins\.trac\.wordpress\.org/browser/([^/]+)`) + svnRegex = regexp.MustCompile(`plugins\.svn\.wordpress\.org/([^/]+)`) + wordfenceRegex = regexp.MustCompile(`wordfence\.com/threat-intel/vulnerabilities/wordpress-plugins/([^/]+)`) + wpOrgRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) +) + +func extractSlugFromOSVRefs(refs []*osvschema.Reference) string { + var tracSlug, svnSlug, wordfenceSlug, wpOrgSlug string + + for _, ref := range refs { + url := ref.GetUrl() + if match := tracRegex.FindStringSubmatch(url); match != nil { + tracSlug = match[1] + } + if match := svnRegex.FindStringSubmatch(url); match != nil { + svnSlug = match[1] + } + if match := wordfenceRegex.FindStringSubmatch(url); match != nil { + wordfenceSlug = match[1] + } + if match := wpOrgRegex.FindStringSubmatch(url); match != nil { + wpOrgSlug = match[1] + } + } + + // Prioritize trac slug + if tracSlug != "" { + return tracSlug + } + if svnSlug != "" { + return svnSlug + } + if wordfenceSlug != "" { + return wordfenceSlug + } + if wpOrgSlug != "" { + return wpOrgSlug + } + + return "" +} + +func normalizeVersion(v string) string { + return strings.TrimPrefix(v, "v") +} + +// ExtractVersions for WordfenceVersionExtractor. +func (w *WordfenceVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { + // Normalize versions in cve first + for i := range cve.Containers.CNA.Affected { + for j := range cve.Containers.CNA.Affected[i].Versions { + vers := &cve.Containers.CNA.Affected[i].Versions[j] + vers.Version = normalizeVersion(vers.Version) + vers.LessThan = normalizeVersion(vers.LessThan) + vers.LessThanOrEqual = normalizeVersion(vers.LessThanOrEqual) + } + } + + // 1. Try standard extraction (which prefers GIT ranges) + w.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) + + slug := extractSlugFromOSVRefs(v.References) + + // 2. If we have Affected entries, update them with slug/ecosystem if missing. + if len(v.Affected) > 0 { + if slug != "" { + for _, aff := range v.Affected { + if aff.Package == nil { + aff.Package = &osvschema.Package{ + Ecosystem: string(osvconstants.EcosystemWordPress), + Name: slug, + } + } + } + } + // Do not return early, we want both GIT and ECOSYSTEM ranges if they exist. + } + + // 3. We also want to produce ECOSYSTEM ranges if possible. + if slug == "" { + if len(v.Affected) == 0 { + metrics.AddNote("Failed to extract versions via default, and no WordPress slug found to attempt fallback") + } else { + metrics.AddNote("No WordPress slug found to attempt generating ECOSYSTEM ranges") + } + return + } + + metrics.AddNote("Attempting to generate ECOSYSTEM ranges for WordPress") + + gotVersions := false + var allRanges []*osvschema.Range + + for _, cveAff := range cve.Containers.CNA.Affected { + versionRanges, _ := w.FindNormalAffectedRanges(cveAff, metrics) + for _, r := range versionRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + } + + if len(allRanges) > 0 { + gotVersions = true + metrics.AddSource(models.VersionSourceAffected) + } + + // CPE Fallback + if !gotVersions { + versionRanges, _ := cpeVersionExtraction(cve, metrics) + for _, r := range versionRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + if len(allRanges) > 0 { + gotVersions = true + } + } + + // Description Fallback + if !gotVersions { + textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) + for _, r := range textRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + if len(allRanges) > 0 { + gotVersions = true + } + } + + if gotVersions { + aff := &osvschema.Affected{ + Package: &osvschema.Package{ + Ecosystem: string(osvconstants.EcosystemWordPress), + Name: slug, + }, + Ranges: allRanges, + } + c.AddAffected(v, aff, metrics) + metrics.Outcome = models.Successful // Override outcome directly + } +} From faa1f59a5583ff08341e1fa6ba973aa6ca906845 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Wed, 15 Jul 2026 06:18:58 +0000 Subject: [PATCH 02/13] no packageinfo on git --- vulnfeeds/conversion/cve5/wordfence_extractor.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vulnfeeds/conversion/cve5/wordfence_extractor.go b/vulnfeeds/conversion/cve5/wordfence_extractor.go index 41940159f0c..a3a95b3d245 100644 --- a/vulnfeeds/conversion/cve5/wordfence_extractor.go +++ b/vulnfeeds/conversion/cve5/wordfence_extractor.go @@ -86,6 +86,17 @@ func (w *WordfenceVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vu if len(v.Affected) > 0 { if slug != "" { for _, aff := range v.Affected { + isGit := false + for _, r := range aff.Ranges { + if r.Type == osvschema.Range_GIT { + isGit = true + break + } + } + if isGit { + aff.Package = nil // Do not put package info on GIT ranges + continue + } if aff.Package == nil { aff.Package = &osvschema.Package{ Ecosystem: string(osvconstants.EcosystemWordPress), From 5dd81f380ead83dda108d1c3db8d8b126ae1892c Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Wed, 29 Jul 2026 05:00:04 +0000 Subject: [PATCH 03/13] patchstack --- vulnfeeds/conversion/cve5/extraction.go | 2 + .../conversion/cve5/patchstack_extractor.go | 172 ++++++++++++++++++ vulnfeeds/conversion/cve5/strategies.go | 14 +- .../cve5/version_extraction_test.go | 39 ++++ vulnfeeds/models/cve.go | 16 +- 5 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 vulnfeeds/conversion/cve5/patchstack_extractor.go diff --git a/vulnfeeds/conversion/cve5/extraction.go b/vulnfeeds/conversion/cve5/extraction.go index 599efb677f1..b607ff25677 100644 --- a/vulnfeeds/conversion/cve5/extraction.go +++ b/vulnfeeds/conversion/cve5/extraction.go @@ -18,6 +18,8 @@ func GetVersionExtractor(cna string) VersionExtractor { return &LinuxVersionExtractor{} case "Wordfence": return &WordfenceVersionExtractor{} + case "Patchstack": + return &PatchstackVersionExtractor{} default: return &DefaultVersionExtractor{} } diff --git a/vulnfeeds/conversion/cve5/patchstack_extractor.go b/vulnfeeds/conversion/cve5/patchstack_extractor.go new file mode 100644 index 00000000000..e0e0ba7cd68 --- /dev/null +++ b/vulnfeeds/conversion/cve5/patchstack_extractor.go @@ -0,0 +1,172 @@ +package cve5 + +import ( + "regexp" + "strings" + + c "github.com/google/osv/vulnfeeds/conversion" + "github.com/google/osv/vulnfeeds/models" + "github.com/google/osv/vulnfeeds/vulns" + "github.com/ossf/osv-schema/bindings/go/osvschema" +) + +// PatchstackVersionExtractor provides version extraction logic for Patchstack CVEs. +type PatchstackVersionExtractor struct { + DefaultVersionExtractor +} + +var _ VersionExtractor = &PatchstackVersionExtractor{} + +var ( + patchstackVulnRegex = regexp.MustCompile(`patchstack\.com/database/vulnerability/([^/]+)`) + patchstackPluginRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/plugin/([^/]+)`) + patchstackThemeRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/theme/([^/]+)`) + wpPluginsRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) + wpThemesRegex = regexp.MustCompile(`wordpress\.org/themes/([^/]+)`) +) + +func extractSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) (string, string) { + var slug string + var ecosystem string = "WordPress" // Default/Fallback + + // 1. Try packageName and collectionURL + if len(cve.Containers.CNA.Affected) > 0 { + aff := cve.Containers.CNA.Affected[0] + if aff.PackageName != "" { + slug = aff.PackageName + } + if aff.CollectionURL == "https://wordpress.org/themes" { + ecosystem = "WordPress:Theme" + } else if aff.CollectionURL == "https://wordpress.org/plugins" { + ecosystem = "WordPress:Plugin" + } + } + + // 2. Fallback to URLs if slug is missing or ecosystem is generic + if slug == "" || ecosystem == "WordPress" { + for _, ref := range v.References { + url := ref.GetUrl() + + if slug == "" { + if match := patchstackVulnRegex.FindStringSubmatch(url); match != nil { + slug = match[1] + } else if match := patchstackPluginRegex.FindStringSubmatch(url); match != nil { + slug = match[1] + if ecosystem == "WordPress" { + ecosystem = "WordPress:Plugin" + } + } else if match := patchstackThemeRegex.FindStringSubmatch(url); match != nil { + slug = match[1] + if ecosystem == "WordPress" { + ecosystem = "WordPress:Theme" + } + } else if match := wpPluginsRegex.FindStringSubmatch(url); match != nil { + slug = match[1] + if ecosystem == "WordPress" { + ecosystem = "WordPress:Plugin" + } + } else if match := wpThemesRegex.FindStringSubmatch(url); match != nil { + slug = match[1] + if ecosystem == "WordPress" { + ecosystem = "WordPress:Theme" + } + } + } + + // If slug already found via packageName, but ecosystem is still generic, try to infer from URLs + if ecosystem == "WordPress" { + if strings.Contains(url, "/theme/") || strings.Contains(url, "/themes/") { + ecosystem = "WordPress:Theme" + } else if strings.Contains(url, "/plugin/") || strings.Contains(url, "/plugins/") { + ecosystem = "WordPress:Plugin" + } + } + } + } + + return slug, ecosystem +} + +// ExtractVersions for PatchstackVersionExtractor. +func (p *PatchstackVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { + // 1. Run default extraction first + p.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) + + // 2. Extract slug and determine ecosystem + slug, ecosystem := extractSlugAndEcosystem(cve, v) + + if slug == "" { + metrics.AddNote("Could not extract slug for Patchstack record") + return + } + + // 3. Update affected packages with correct ecosystem and slug + if len(v.Affected) > 0 { + for _, aff := range v.Affected { + isGit := false + for _, r := range aff.Ranges { + if r.Type == osvschema.Range_GIT { + isGit = true + break + } + } + if isGit { + continue + } + + if aff.Package == nil { + aff.Package = &osvschema.Package{ + Ecosystem: ecosystem, + Name: slug, + } + } + } + } + + // 4. If default extraction didn't produce anything (e.g. no git ranges found), + // we still want to produce ECOSYSTEM ranges for WordPress. + // This mirrors Wordfence extractor logic. + if len(v.Affected) == 0 { + metrics.AddNote("Attempting to generate ECOSYSTEM ranges for Patchstack") + + gotVersions := false + var allRanges []*osvschema.Range + + for _, cveAff := range cve.Containers.CNA.Affected { + versionRanges, _ := p.FindNormalAffectedRanges(cveAff, metrics) + for _, r := range versionRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + } + + if len(allRanges) > 0 { + gotVersions = true + metrics.AddSource(models.VersionSourceAffected) + } + + // Description Fallback (if still no versions) + if !gotVersions { + textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) + for _, r := range textRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + if len(allRanges) > 0 { + gotVersions = true + } + } + + if gotVersions { + aff := &osvschema.Affected{ + Package: &osvschema.Package{ + Ecosystem: ecosystem, + Name: slug, + }, + Ranges: allRanges, + } + c.AddAffected(v, aff, metrics) + metrics.Outcome = models.Successful // Override outcome directly + } + } +} diff --git a/vulnfeeds/conversion/cve5/strategies.go b/vulnfeeds/conversion/cve5/strategies.go index 95b5babd448..76fd355692f 100644 --- a/vulnfeeds/conversion/cve5/strategies.go +++ b/vulnfeeds/conversion/cve5/strategies.go @@ -58,7 +58,19 @@ func initialNormalExtraction(vers models.Versions, metrics *models.ConversionMet introduced = vers.Version metrics.AddNote("%s - Introduced from version value - %s", vQuality.String(), vers.Version) } - if vLessThanQual.AtLeast(acceptableQuality) { + // Prefer changes for fixed version if available. + var fixedFromChanges string + for _, ch := range vers.Changes { + if ch.Status == "unaffected" && ch.At != "" { + fixedFromChanges = ch.At + break + } + } + + if fixedFromChanges != "" { + fixed = fixedFromChanges + metrics.AddNote("Fixed from changes - %s", fixed) + } else if vLessThanQual.AtLeast(acceptableQuality) { fixed = vers.LessThan metrics.AddNote("%s - Fixed from LessThan value - %s", vLessThanQual.String(), vers.LessThan) } else if vLTOEQual.AtLeast(acceptableQuality) { diff --git a/vulnfeeds/conversion/cve5/version_extraction_test.go b/vulnfeeds/conversion/cve5/version_extraction_test.go index 575c0ece5c3..21256d32e98 100644 --- a/vulnfeeds/conversion/cve5/version_extraction_test.go +++ b/vulnfeeds/conversion/cve5/version_extraction_test.go @@ -149,6 +149,26 @@ func TestFindNormalAffectedRanges(t *testing.T) { }, wantRangeType: VersionRangeTypeGit, }, + { + name: "changes preferred over lessThanOrEqual with filler version", + affected: models.Affected{ + Versions: []models.Versions{ + { + Status: "affected", + Version: "n/a", + LessThanOrEqual: "1.0.32", + Changes: []models.Change{ + {At: "1.0.33", Status: "unaffected"}, + }, + VersionType: "custom", + }, + }, + }, + wantRanges: []*osvschema.Range{ + conversion.BuildVersionRange("0", "", "1.0.33"), + }, + wantRangeType: VersionRangeTypeEcosystem, + }, } for _, tt := range tests { @@ -621,6 +641,25 @@ func TestExtractVersions(t *testing.T) { }}, }}, }, + { + name: "CVE-2021-23209", + cve: loadTestData(t, "CVE-2021-23209"), + cnaAssigner: "Patchstack", + repos: []string{}, + expectedAffected: []*osvschema.Affected{{ + Package: &osvschema.Package{ + Ecosystem: "WordPress:Plugin", + Name: "accelerated-mobile-pages", + }, + Ranges: []*osvschema.Range{{ + Type: osvschema.Range_ECOSYSTEM, + Events: []*osvschema.Event{ + {Introduced: "0"}, + {Fixed: "1.0.77.33"}, + }, + }}, + }}, + }, } for _, tc := range testCases { diff --git a/vulnfeeds/models/cve.go b/vulnfeeds/models/cve.go index f2393c004a1..cd9b34991bb 100644 --- a/vulnfeeds/models/cve.go +++ b/vulnfeeds/models/cve.go @@ -126,12 +126,18 @@ type Affected struct { DefaultStatus string `json:"defaultStatus,omitempty"` } +type Change struct { + At string `json:"at,omitempty"` + Status string `json:"status,omitempty"` +} + type Versions struct { - Version string `json:"version,omitempty"` - Status string `json:"status,omitempty"` - LessThanOrEqual string `json:"lessThanOrEqual,omitempty"` - LessThan string `json:"lessThan,omitempty"` - VersionType string `json:"versionType,omitempty"` + Version string `json:"version,omitempty"` + Status string `json:"status,omitempty"` + LessThanOrEqual string `json:"lessThanOrEqual,omitempty"` + LessThan string `json:"lessThan,omitempty"` + VersionType string `json:"versionType,omitempty"` + Changes []Change `json:"changes,omitempty"` } type CVE5 struct { From e8c3e91283752daca0f9794e11efaac25a8428a0 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Wed, 29 Jul 2026 05:08:25 +0000 Subject: [PATCH 04/13] refactor to share wordpress slug logic and add wpscan --- vulnfeeds/conversion/cve5/extraction.go | 2 + .../conversion/cve5/patchstack_extractor.go | 76 +-------- .../cve5/version_extraction_test.go | 21 ++- .../conversion/cve5/wordfence_extractor.go | 51 +----- vulnfeeds/conversion/cve5/wordpress.go | 147 ++++++++++++++++++ vulnfeeds/conversion/cve5/wpscan_extractor.go | 105 +++++++++++++ 6 files changed, 279 insertions(+), 123 deletions(-) create mode 100644 vulnfeeds/conversion/cve5/wordpress.go create mode 100644 vulnfeeds/conversion/cve5/wpscan_extractor.go diff --git a/vulnfeeds/conversion/cve5/extraction.go b/vulnfeeds/conversion/cve5/extraction.go index b607ff25677..c8f472119c5 100644 --- a/vulnfeeds/conversion/cve5/extraction.go +++ b/vulnfeeds/conversion/cve5/extraction.go @@ -20,6 +20,8 @@ func GetVersionExtractor(cna string) VersionExtractor { return &WordfenceVersionExtractor{} case "Patchstack": return &PatchstackVersionExtractor{} + case "WPScan": + return &WPScanVersionExtractor{} default: return &DefaultVersionExtractor{} } diff --git a/vulnfeeds/conversion/cve5/patchstack_extractor.go b/vulnfeeds/conversion/cve5/patchstack_extractor.go index e0e0ba7cd68..1813a5a7aee 100644 --- a/vulnfeeds/conversion/cve5/patchstack_extractor.go +++ b/vulnfeeds/conversion/cve5/patchstack_extractor.go @@ -1,9 +1,6 @@ package cve5 import ( - "regexp" - "strings" - c "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/models" "github.com/google/osv/vulnfeeds/vulns" @@ -17,83 +14,14 @@ type PatchstackVersionExtractor struct { var _ VersionExtractor = &PatchstackVersionExtractor{} -var ( - patchstackVulnRegex = regexp.MustCompile(`patchstack\.com/database/vulnerability/([^/]+)`) - patchstackPluginRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/plugin/([^/]+)`) - patchstackThemeRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/theme/([^/]+)`) - wpPluginsRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) - wpThemesRegex = regexp.MustCompile(`wordpress\.org/themes/([^/]+)`) -) - -func extractSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) (string, string) { - var slug string - var ecosystem string = "WordPress" // Default/Fallback - - // 1. Try packageName and collectionURL - if len(cve.Containers.CNA.Affected) > 0 { - aff := cve.Containers.CNA.Affected[0] - if aff.PackageName != "" { - slug = aff.PackageName - } - if aff.CollectionURL == "https://wordpress.org/themes" { - ecosystem = "WordPress:Theme" - } else if aff.CollectionURL == "https://wordpress.org/plugins" { - ecosystem = "WordPress:Plugin" - } - } - - // 2. Fallback to URLs if slug is missing or ecosystem is generic - if slug == "" || ecosystem == "WordPress" { - for _, ref := range v.References { - url := ref.GetUrl() - - if slug == "" { - if match := patchstackVulnRegex.FindStringSubmatch(url); match != nil { - slug = match[1] - } else if match := patchstackPluginRegex.FindStringSubmatch(url); match != nil { - slug = match[1] - if ecosystem == "WordPress" { - ecosystem = "WordPress:Plugin" - } - } else if match := patchstackThemeRegex.FindStringSubmatch(url); match != nil { - slug = match[1] - if ecosystem == "WordPress" { - ecosystem = "WordPress:Theme" - } - } else if match := wpPluginsRegex.FindStringSubmatch(url); match != nil { - slug = match[1] - if ecosystem == "WordPress" { - ecosystem = "WordPress:Plugin" - } - } else if match := wpThemesRegex.FindStringSubmatch(url); match != nil { - slug = match[1] - if ecosystem == "WordPress" { - ecosystem = "WordPress:Theme" - } - } - } - - // If slug already found via packageName, but ecosystem is still generic, try to infer from URLs - if ecosystem == "WordPress" { - if strings.Contains(url, "/theme/") || strings.Contains(url, "/themes/") { - ecosystem = "WordPress:Theme" - } else if strings.Contains(url, "/plugin/") || strings.Contains(url, "/plugins/") { - ecosystem = "WordPress:Plugin" - } - } - } - } - - return slug, ecosystem -} // ExtractVersions for PatchstackVersionExtractor. func (p *PatchstackVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { // 1. Run default extraction first p.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) - // 2. Extract slug and determine ecosystem - slug, ecosystem := extractSlugAndEcosystem(cve, v) + // 2. Extract slug and determine ecosystem using shared helper + slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) if slug == "" { metrics.AddNote("Could not extract slug for Patchstack record") diff --git a/vulnfeeds/conversion/cve5/version_extraction_test.go b/vulnfeeds/conversion/cve5/version_extraction_test.go index 21256d32e98..567396520f9 100644 --- a/vulnfeeds/conversion/cve5/version_extraction_test.go +++ b/vulnfeeds/conversion/cve5/version_extraction_test.go @@ -629,7 +629,7 @@ func TestExtractVersions(t *testing.T) { repos: []string{}, expectedAffected: []*osvschema.Affected{{ Package: &osvschema.Package{ - Ecosystem: "WordPress", + Ecosystem: "WordPress:Plugin", Name: "wordpress-seo", }, Ranges: []*osvschema.Range{{ @@ -660,6 +660,25 @@ func TestExtractVersions(t *testing.T) { }}, }}, }, + { + name: "CVE-2015-10001", + cve: loadTestData(t, "CVE-2015-10001"), + cnaAssigner: "WPScan", + repos: []string{}, + expectedAffected: []*osvschema.Affected{{ + Package: &osvschema.Package{ + Ecosystem: "WordPress:Plugin", + Name: "wp-stats", + }, + Ranges: []*osvschema.Range{{ + Type: osvschema.Range_ECOSYSTEM, + Events: []*osvschema.Event{ + {Introduced: "2.52"}, + {LastAffected: "2.52"}, + }, + }}, + }}, + }, } for _, tc := range testCases { diff --git a/vulnfeeds/conversion/cve5/wordfence_extractor.go b/vulnfeeds/conversion/cve5/wordfence_extractor.go index a3a95b3d245..61f56573e6f 100644 --- a/vulnfeeds/conversion/cve5/wordfence_extractor.go +++ b/vulnfeeds/conversion/cve5/wordfence_extractor.go @@ -1,13 +1,11 @@ package cve5 import ( - "regexp" "strings" c "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/models" "github.com/google/osv/vulnfeeds/vulns" - "github.com/ossf/osv-schema/bindings/go/osvconstants" "github.com/ossf/osv-schema/bindings/go/osvschema" ) @@ -18,49 +16,6 @@ type WordfenceVersionExtractor struct { var _ VersionExtractor = &WordfenceVersionExtractor{} -var ( - tracRegex = regexp.MustCompile(`plugins\.trac\.wordpress\.org/browser/([^/]+)`) - svnRegex = regexp.MustCompile(`plugins\.svn\.wordpress\.org/([^/]+)`) - wordfenceRegex = regexp.MustCompile(`wordfence\.com/threat-intel/vulnerabilities/wordpress-plugins/([^/]+)`) - wpOrgRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) -) - -func extractSlugFromOSVRefs(refs []*osvschema.Reference) string { - var tracSlug, svnSlug, wordfenceSlug, wpOrgSlug string - - for _, ref := range refs { - url := ref.GetUrl() - if match := tracRegex.FindStringSubmatch(url); match != nil { - tracSlug = match[1] - } - if match := svnRegex.FindStringSubmatch(url); match != nil { - svnSlug = match[1] - } - if match := wordfenceRegex.FindStringSubmatch(url); match != nil { - wordfenceSlug = match[1] - } - if match := wpOrgRegex.FindStringSubmatch(url); match != nil { - wpOrgSlug = match[1] - } - } - - // Prioritize trac slug - if tracSlug != "" { - return tracSlug - } - if svnSlug != "" { - return svnSlug - } - if wordfenceSlug != "" { - return wordfenceSlug - } - if wpOrgSlug != "" { - return wpOrgSlug - } - - return "" -} - func normalizeVersion(v string) string { return strings.TrimPrefix(v, "v") } @@ -80,7 +35,7 @@ func (w *WordfenceVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vu // 1. Try standard extraction (which prefers GIT ranges) w.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) - slug := extractSlugFromOSVRefs(v.References) + slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) // 2. If we have Affected entries, update them with slug/ecosystem if missing. if len(v.Affected) > 0 { @@ -99,7 +54,7 @@ func (w *WordfenceVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vu } if aff.Package == nil { aff.Package = &osvschema.Package{ - Ecosystem: string(osvconstants.EcosystemWordPress), + Ecosystem: ecosystem, Name: slug, } } @@ -163,7 +118,7 @@ func (w *WordfenceVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vu if gotVersions { aff := &osvschema.Affected{ Package: &osvschema.Package{ - Ecosystem: string(osvconstants.EcosystemWordPress), + Ecosystem: ecosystem, Name: slug, }, Ranges: allRanges, diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go new file mode 100644 index 00000000000..33e4fce3529 --- /dev/null +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -0,0 +1,147 @@ +package cve5 + +import ( + "regexp" + "strings" + + "github.com/google/osv/vulnfeeds/models" + "github.com/google/osv/vulnfeeds/vulns" +) + +var ( + wpPluginTracRegex = regexp.MustCompile(`plugins\.trac\.wordpress\.org/browser/([^/]+)`) + wpPluginSvnRegex = regexp.MustCompile(`plugins\.svn\.wordpress\.org/([^/]+)`) + wordfencePluginRegex = regexp.MustCompile(`wordfence\.com/threat-intel/vulnerabilities/wordpress-plugins/([^/]+)`) + wpPluginOrgRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) + wpThemeOrgRegex = regexp.MustCompile(`wordpress\.org/themes/([^/]+)`) + + patchstackVulnRegex = regexp.MustCompile(`patchstack\.com/database/vulnerability/([^/]+)`) + patchstackPluginRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/plugin/([^/]+)`) + patchstackThemeRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/theme/([^/]+)`) +) + +// extractWordPressSlugAndEcosystem unifies the logic to extract the slug and determine +// the specific WordPress ecosystem (Core, Plugin, Theme) for a given CVE. +func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) (string, string) { + var slug string + var ecosystem string = "WordPress" // Default/Fallback + + // 1. Core Check (Highest Priority) + if len(cve.Containers.CNA.Affected) > 0 { + aff := cve.Containers.CNA.Affected[0] + if strings.EqualFold(aff.Vendor, "wordpress") && strings.EqualFold(aff.Product, "wordpress") { + return "wordpress", "WordPress:Core" + } + } + + // 2. Slug from PackageName + if len(cve.Containers.CNA.Affected) > 0 { + aff := cve.Containers.CNA.Affected[0] + if aff.PackageName != "" { + slug = aff.PackageName + } + } + + // 3. Ecosystem Extraction from CollectionURL + if len(cve.Containers.CNA.Affected) > 0 { + aff := cve.Containers.CNA.Affected[0] + if aff.CollectionURL == "https://wordpress.org/themes" { + ecosystem = "WordPress:Theme" + } else if aff.CollectionURL == "https://wordpress.org/plugins" { + ecosystem = "WordPress:Plugin" + } + } + + // 4. URL Heuristics Fallback (for slug and ecosystem) + var tracSlug, svnSlug, wordfenceSlug, wpOrgPluginSlug, wpOrgThemeSlug, patchstackPluginSlug, patchstackThemeSlug, patchstackVulnSlug string + var urlEcosystem string + + for _, ref := range v.References { + url := ref.GetUrl() + + if match := wpPluginTracRegex.FindStringSubmatch(url); match != nil { + tracSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := wpPluginSvnRegex.FindStringSubmatch(url); match != nil { + svnSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := wordfencePluginRegex.FindStringSubmatch(url); match != nil { + wordfenceSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := wpPluginOrgRegex.FindStringSubmatch(url); match != nil { + wpOrgPluginSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := wpThemeOrgRegex.FindStringSubmatch(url); match != nil { + wpOrgThemeSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Theme" } + } else if match := patchstackPluginRegex.FindStringSubmatch(url); match != nil { + patchstackPluginSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := patchstackThemeRegex.FindStringSubmatch(url); match != nil { + patchstackThemeSlug = match[1] + if urlEcosystem == "" { urlEcosystem = "WordPress:Theme" } + } else if match := patchstackVulnRegex.FindStringSubmatch(url); match != nil { + patchstackVulnSlug = match[1] + } + + // Generic URL keyword check for ecosystem if still generic + if urlEcosystem == "" { + if strings.Contains(url, "/theme/") || strings.Contains(url, "/themes/") { + urlEcosystem = "WordPress:Theme" + } else if strings.Contains(url, "/plugin/") || strings.Contains(url, "/plugins/") { + urlEcosystem = "WordPress:Plugin" + } + } + } + + var urlSlug string + if tracSlug != "" { + urlSlug = tracSlug + } else if svnSlug != "" { + urlSlug = svnSlug + } else if wordfenceSlug != "" { + urlSlug = wordfenceSlug + } else if wpOrgPluginSlug != "" { + urlSlug = wpOrgPluginSlug + } else if wpOrgThemeSlug != "" { + urlSlug = wpOrgThemeSlug + } else if patchstackPluginSlug != "" { + urlSlug = patchstackPluginSlug + } else if patchstackThemeSlug != "" { + urlSlug = patchstackThemeSlug + } else if patchstackVulnSlug != "" { + urlSlug = patchstackVulnSlug + } + + if slug == "" && urlSlug != "" { + slug = urlSlug + } + + // Slug from Product Fallback + if slug == "" && len(cve.Containers.CNA.Affected) > 0 { + aff := cve.Containers.CNA.Affected[0] + if vulns.CheckQuality(aff.Product).AtLeast(vulns.Spaces) { + // Basic slugification (lowercase, replace spaces with hyphens) + slug = strings.ToLower(aff.Product) + slug = strings.ReplaceAll(slug, " ", "-") + } + } + + if ecosystem == "WordPress" && urlEcosystem != "" { + ecosystem = urlEcosystem + } + + // 5. Description/Title Heuristics Fallback + if ecosystem == "WordPress" { + desc := strings.ToLower(models.EnglishDescription(cve.Containers.CNA.Descriptions)) + title := strings.ToLower(cve.Containers.CNA.Title) + + if strings.Contains(desc, "plugin") || strings.Contains(title, "plugin") { + ecosystem = "WordPress:Plugin" + } else if strings.Contains(desc, "theme") || strings.Contains(title, "theme") { + ecosystem = "WordPress:Theme" + } + } + + return slug, ecosystem +} diff --git a/vulnfeeds/conversion/cve5/wpscan_extractor.go b/vulnfeeds/conversion/cve5/wpscan_extractor.go new file mode 100644 index 00000000000..c5bd6c97ddf --- /dev/null +++ b/vulnfeeds/conversion/cve5/wpscan_extractor.go @@ -0,0 +1,105 @@ +package cve5 + +import ( + c "github.com/google/osv/vulnfeeds/conversion" + "github.com/google/osv/vulnfeeds/models" + "github.com/google/osv/vulnfeeds/vulns" + "github.com/ossf/osv-schema/bindings/go/osvschema" +) + +// WPScanVersionExtractor provides version extraction logic for WPScan CVEs. +type WPScanVersionExtractor struct { + DefaultVersionExtractor +} + +var _ VersionExtractor = &WPScanVersionExtractor{} + +// ExtractVersions for WPScanVersionExtractor. +func (w *WPScanVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { + // 1. Run default extraction first + w.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) + + // 2. Extract slug and determine ecosystem using shared helper + slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) + + if slug == "" { + metrics.AddNote("Could not extract slug for WPScan record") + // We might still want to assign the ecosystem if we can determine it + } + + // 3. Update affected packages with correct ecosystem and slug + if len(v.Affected) > 0 { + for _, aff := range v.Affected { + isGit := false + for _, r := range aff.Ranges { + if r.Type == osvschema.Range_GIT { + isGit = true + break + } + } + if isGit { + continue + } + + if aff.Package == nil { + aff.Package = &osvschema.Package{ + Ecosystem: ecosystem, + Name: slug, + } + } else { + // Update ecosystem if it was generic + if aff.Package.Ecosystem == "WordPress" || aff.Package.Ecosystem == "" { + aff.Package.Ecosystem = ecosystem + } + if aff.Package.Name == "" { + aff.Package.Name = slug + } + } + } + } + + // 4. If default extraction didn't produce anything, try to generate ECOSYSTEM ranges. + if len(v.Affected) == 0 { + metrics.AddNote("Attempting to generate ECOSYSTEM ranges for WPScan") + + gotVersions := false + var allRanges []*osvschema.Range + + for _, cveAff := range cve.Containers.CNA.Affected { + versionRanges, _ := w.FindNormalAffectedRanges(cveAff, metrics) + for _, r := range versionRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + } + + if len(allRanges) > 0 { + gotVersions = true + metrics.AddSource(models.VersionSourceAffected) + } + + // Description Fallback + if !gotVersions { + textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) + for _, r := range textRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + if len(allRanges) > 0 { + gotVersions = true + } + } + + if gotVersions { + aff := &osvschema.Affected{ + Package: &osvschema.Package{ + Ecosystem: ecosystem, + Name: slug, + }, + Ranges: allRanges, + } + c.AddAffected(v, aff, metrics) + metrics.Outcome = models.Successful + } + } +} From 2b702fb17d907b6ad52b32a4c7b333b443fad27e Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 30 Jul 2026 01:49:27 +0000 Subject: [PATCH 05/13] add useful links --- .../conversion/cve5/patchstack_extractor.go | 28 +++++++++++++++++-- vulnfeeds/conversion/cve5/wordpress.go | 5 ++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/vulnfeeds/conversion/cve5/patchstack_extractor.go b/vulnfeeds/conversion/cve5/patchstack_extractor.go index 1813a5a7aee..58e79bdd7a0 100644 --- a/vulnfeeds/conversion/cve5/patchstack_extractor.go +++ b/vulnfeeds/conversion/cve5/patchstack_extractor.go @@ -1,6 +1,8 @@ package cve5 import ( + "slices" + c "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/models" "github.com/google/osv/vulnfeeds/vulns" @@ -14,7 +16,6 @@ type PatchstackVersionExtractor struct { var _ VersionExtractor = &PatchstackVersionExtractor{} - // ExtractVersions for PatchstackVersionExtractor. func (p *PatchstackVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { // 1. Run default extraction first @@ -23,7 +24,30 @@ func (p *PatchstackVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.V // 2. Extract slug and determine ecosystem using shared helper slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) - if slug == "" { + if slug != "" { + var baseURL string + switch ecosystem { + case "WordPress:Plugin": + baseURL = "https://wordpress.org/plugins/" + case "WordPress:Theme": + baseURL = "https://wordpress.org/themes/" + } + + if baseURL != "" { + wpURL := baseURL + slug + "/" + // Check if already exists to avoid duplicates + exists := slices.ContainsFunc(v.References, func(ref *osvschema.Reference) bool { + return ref.GetUrl() == wpURL + }) + if !exists { + v.References = append(v.References, &osvschema.Reference{ + Type: osvschema.Reference_WEB, + Url: wpURL, + }) + metrics.AddNote("Added wordpress.org reference link: %s", wpURL) + } + } + } else { metrics.AddNote("Could not extract slug for Patchstack record") return } diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go index 33e4fce3529..e8d61ad51c0 100644 --- a/vulnfeeds/conversion/cve5/wordpress.go +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -45,9 +45,10 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( // 3. Ecosystem Extraction from CollectionURL if len(cve.Containers.CNA.Affected) > 0 { aff := cve.Containers.CNA.Affected[0] - if aff.CollectionURL == "https://wordpress.org/themes" { + switch aff.CollectionURL { + case "https://wordpress.org/themes": ecosystem = "WordPress:Theme" - } else if aff.CollectionURL == "https://wordpress.org/plugins" { + case "https://wordpress.org/plugins": ecosystem = "WordPress:Plugin" } } From f27ff3a20a57d84b7ee33aeb9b3c1311b6efa2f9 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 30 Jul 2026 03:48:16 +0000 Subject: [PATCH 06/13] turn the wordpress cnas into handlers to unify extractors --- vulnfeeds/conversion/cve5/extraction.go | 6 +- .../conversion/cve5/patchstack_extractor.go | 124 ---------- .../cve5/version_extraction_test.go | 2 +- .../conversion/cve5/wordfence_extractor.go | 129 ---------- vulnfeeds/conversion/cve5/wordpress.go | 228 ++++++++++++++++-- vulnfeeds/conversion/cve5/wpscan_extractor.go | 105 -------- 6 files changed, 216 insertions(+), 378 deletions(-) delete mode 100644 vulnfeeds/conversion/cve5/patchstack_extractor.go delete mode 100644 vulnfeeds/conversion/cve5/wordfence_extractor.go delete mode 100644 vulnfeeds/conversion/cve5/wpscan_extractor.go diff --git a/vulnfeeds/conversion/cve5/extraction.go b/vulnfeeds/conversion/cve5/extraction.go index c8f472119c5..50be1f27998 100644 --- a/vulnfeeds/conversion/cve5/extraction.go +++ b/vulnfeeds/conversion/cve5/extraction.go @@ -17,11 +17,11 @@ func GetVersionExtractor(cna string) VersionExtractor { case "Linux": return &LinuxVersionExtractor{} case "Wordfence": - return &WordfenceVersionExtractor{} + return &WordpressExtractor{Handler: &WordfenceHandler{}} case "Patchstack": - return &PatchstackVersionExtractor{} + return &WordpressExtractor{Handler: &PatchstackHandler{}} case "WPScan": - return &WPScanVersionExtractor{} + return &WordpressExtractor{Handler: &WPScanHandler{}} default: return &DefaultVersionExtractor{} } diff --git a/vulnfeeds/conversion/cve5/patchstack_extractor.go b/vulnfeeds/conversion/cve5/patchstack_extractor.go deleted file mode 100644 index 58e79bdd7a0..00000000000 --- a/vulnfeeds/conversion/cve5/patchstack_extractor.go +++ /dev/null @@ -1,124 +0,0 @@ -package cve5 - -import ( - "slices" - - c "github.com/google/osv/vulnfeeds/conversion" - "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/vulns" - "github.com/ossf/osv-schema/bindings/go/osvschema" -) - -// PatchstackVersionExtractor provides version extraction logic for Patchstack CVEs. -type PatchstackVersionExtractor struct { - DefaultVersionExtractor -} - -var _ VersionExtractor = &PatchstackVersionExtractor{} - -// ExtractVersions for PatchstackVersionExtractor. -func (p *PatchstackVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { - // 1. Run default extraction first - p.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) - - // 2. Extract slug and determine ecosystem using shared helper - slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) - - if slug != "" { - var baseURL string - switch ecosystem { - case "WordPress:Plugin": - baseURL = "https://wordpress.org/plugins/" - case "WordPress:Theme": - baseURL = "https://wordpress.org/themes/" - } - - if baseURL != "" { - wpURL := baseURL + slug + "/" - // Check if already exists to avoid duplicates - exists := slices.ContainsFunc(v.References, func(ref *osvschema.Reference) bool { - return ref.GetUrl() == wpURL - }) - if !exists { - v.References = append(v.References, &osvschema.Reference{ - Type: osvschema.Reference_WEB, - Url: wpURL, - }) - metrics.AddNote("Added wordpress.org reference link: %s", wpURL) - } - } - } else { - metrics.AddNote("Could not extract slug for Patchstack record") - return - } - - // 3. Update affected packages with correct ecosystem and slug - if len(v.Affected) > 0 { - for _, aff := range v.Affected { - isGit := false - for _, r := range aff.Ranges { - if r.Type == osvschema.Range_GIT { - isGit = true - break - } - } - if isGit { - continue - } - - if aff.Package == nil { - aff.Package = &osvschema.Package{ - Ecosystem: ecosystem, - Name: slug, - } - } - } - } - - // 4. If default extraction didn't produce anything (e.g. no git ranges found), - // we still want to produce ECOSYSTEM ranges for WordPress. - // This mirrors Wordfence extractor logic. - if len(v.Affected) == 0 { - metrics.AddNote("Attempting to generate ECOSYSTEM ranges for Patchstack") - - gotVersions := false - var allRanges []*osvschema.Range - - for _, cveAff := range cve.Containers.CNA.Affected { - versionRanges, _ := p.FindNormalAffectedRanges(cveAff, metrics) - for _, r := range versionRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - } - - if len(allRanges) > 0 { - gotVersions = true - metrics.AddSource(models.VersionSourceAffected) - } - - // Description Fallback (if still no versions) - if !gotVersions { - textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) - for _, r := range textRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - if len(allRanges) > 0 { - gotVersions = true - } - } - - if gotVersions { - aff := &osvschema.Affected{ - Package: &osvschema.Package{ - Ecosystem: ecosystem, - Name: slug, - }, - Ranges: allRanges, - } - c.AddAffected(v, aff, metrics) - metrics.Outcome = models.Successful // Override outcome directly - } - } -} diff --git a/vulnfeeds/conversion/cve5/version_extraction_test.go b/vulnfeeds/conversion/cve5/version_extraction_test.go index 567396520f9..1e3238482e2 100644 --- a/vulnfeeds/conversion/cve5/version_extraction_test.go +++ b/vulnfeeds/conversion/cve5/version_extraction_test.go @@ -408,7 +408,7 @@ func TestGetVersionExtractor(t *testing.T) { AssignerShortName: "Wordfence", }, }, - expectedType: reflect.TypeOf(&WordfenceVersionExtractor{}), + expectedType: reflect.TypeOf(&WordpressExtractor{}), }, { name: "Default CVE", diff --git a/vulnfeeds/conversion/cve5/wordfence_extractor.go b/vulnfeeds/conversion/cve5/wordfence_extractor.go deleted file mode 100644 index 61f56573e6f..00000000000 --- a/vulnfeeds/conversion/cve5/wordfence_extractor.go +++ /dev/null @@ -1,129 +0,0 @@ -package cve5 - -import ( - "strings" - - c "github.com/google/osv/vulnfeeds/conversion" - "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/vulns" - "github.com/ossf/osv-schema/bindings/go/osvschema" -) - -// WordfenceVersionExtractor provides version extraction logic for Wordfence CVEs. -type WordfenceVersionExtractor struct { - DefaultVersionExtractor -} - -var _ VersionExtractor = &WordfenceVersionExtractor{} - -func normalizeVersion(v string) string { - return strings.TrimPrefix(v, "v") -} - -// ExtractVersions for WordfenceVersionExtractor. -func (w *WordfenceVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { - // Normalize versions in cve first - for i := range cve.Containers.CNA.Affected { - for j := range cve.Containers.CNA.Affected[i].Versions { - vers := &cve.Containers.CNA.Affected[i].Versions[j] - vers.Version = normalizeVersion(vers.Version) - vers.LessThan = normalizeVersion(vers.LessThan) - vers.LessThanOrEqual = normalizeVersion(vers.LessThanOrEqual) - } - } - - // 1. Try standard extraction (which prefers GIT ranges) - w.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) - - slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) - - // 2. If we have Affected entries, update them with slug/ecosystem if missing. - if len(v.Affected) > 0 { - if slug != "" { - for _, aff := range v.Affected { - isGit := false - for _, r := range aff.Ranges { - if r.Type == osvschema.Range_GIT { - isGit = true - break - } - } - if isGit { - aff.Package = nil // Do not put package info on GIT ranges - continue - } - if aff.Package == nil { - aff.Package = &osvschema.Package{ - Ecosystem: ecosystem, - Name: slug, - } - } - } - } - // Do not return early, we want both GIT and ECOSYSTEM ranges if they exist. - } - - // 3. We also want to produce ECOSYSTEM ranges if possible. - if slug == "" { - if len(v.Affected) == 0 { - metrics.AddNote("Failed to extract versions via default, and no WordPress slug found to attempt fallback") - } else { - metrics.AddNote("No WordPress slug found to attempt generating ECOSYSTEM ranges") - } - return - } - - metrics.AddNote("Attempting to generate ECOSYSTEM ranges for WordPress") - - gotVersions := false - var allRanges []*osvschema.Range - - for _, cveAff := range cve.Containers.CNA.Affected { - versionRanges, _ := w.FindNormalAffectedRanges(cveAff, metrics) - for _, r := range versionRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - } - - if len(allRanges) > 0 { - gotVersions = true - metrics.AddSource(models.VersionSourceAffected) - } - - // CPE Fallback - if !gotVersions { - versionRanges, _ := cpeVersionExtraction(cve, metrics) - for _, r := range versionRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - if len(allRanges) > 0 { - gotVersions = true - } - } - - // Description Fallback - if !gotVersions { - textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) - for _, r := range textRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - if len(allRanges) > 0 { - gotVersions = true - } - } - - if gotVersions { - aff := &osvschema.Affected{ - Package: &osvschema.Package{ - Ecosystem: ecosystem, - Name: slug, - }, - Ranges: allRanges, - } - c.AddAffected(v, aff, metrics) - metrics.Outcome = models.Successful // Override outcome directly - } -} diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go index e8d61ad51c0..21e3b0b521c 100644 --- a/vulnfeeds/conversion/cve5/wordpress.go +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -2,10 +2,13 @@ package cve5 import ( "regexp" + "slices" "strings" + c "github.com/google/osv/vulnfeeds/conversion" "github.com/google/osv/vulnfeeds/models" "github.com/google/osv/vulnfeeds/vulns" + "github.com/ossf/osv-schema/bindings/go/osvschema" ) var ( @@ -96,22 +99,22 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( } var urlSlug string - if tracSlug != "" { - urlSlug = tracSlug - } else if svnSlug != "" { - urlSlug = svnSlug - } else if wordfenceSlug != "" { - urlSlug = wordfenceSlug - } else if wpOrgPluginSlug != "" { - urlSlug = wpOrgPluginSlug - } else if wpOrgThemeSlug != "" { - urlSlug = wpOrgThemeSlug - } else if patchstackPluginSlug != "" { - urlSlug = patchstackPluginSlug - } else if patchstackThemeSlug != "" { - urlSlug = patchstackThemeSlug - } else if patchstackVulnSlug != "" { - urlSlug = patchstackVulnSlug + slugsToTry := []string{ + tracSlug, + svnSlug, + wordfenceSlug, + wpOrgPluginSlug, + wpOrgThemeSlug, + patchstackPluginSlug, + patchstackThemeSlug, + patchstackVulnSlug, + } + + for _, s := range slugsToTry { + if s != "" { + urlSlug = s + break + } } if slug == "" && urlSlug != "" { @@ -146,3 +149,196 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( return slug, ecosystem } + +// WordpressHandler defines hooks for CNA-specific logic. +type WordpressHandler interface { + PreExtract(cve *models.CVE5) + PostExtractDefault(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) +} + +// WordpressExtractor handles version extraction for WordPress CVEs. +type WordpressExtractor struct { + DefaultVersionExtractor + Handler WordpressHandler +} + +var _ VersionExtractor = &WordpressExtractor{} + +func (w *WordpressExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { + if w.Handler != nil { + w.Handler.PreExtract(&cve) + } + + // 1. Run default extraction first + w.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) + + // 2. Extract slug and determine ecosystem using shared helper + slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) + + if w.Handler != nil { + w.Handler.PostExtractDefault(cve, v, metrics, slug, ecosystem) + } + + // 3. Update affected packages with correct ecosystem and slug + if len(v.Affected) > 0 { + for _, aff := range v.Affected { + isGit := false + for _, r := range aff.Ranges { + if r.Type == osvschema.Range_GIT { + isGit = true + break + } + } + if isGit { + aff.Package = nil // Do not put package info on GIT ranges + continue + } + + if slug == "" { + continue // Skip enriching if we have no slug + } + + if aff.Package == nil { + aff.Package = &osvschema.Package{ + Ecosystem: ecosystem, + Name: slug, + } + } else { + // Update ecosystem if it was generic + if aff.Package.Ecosystem == "WordPress" || aff.Package.Ecosystem == "" { + aff.Package.Ecosystem = ecosystem + } + if aff.Package.Name == "" { + aff.Package.Name = slug + } + } + } + } + + // 4. Unified Fallback Strategy + if len(v.Affected) == 0 { + if slug == "" { + metrics.AddNote("Failed to extract versions via default, and no WordPress slug found to attempt fallback") + return + } + + metrics.AddNote("Attempting to generate ECOSYSTEM ranges for WordPress") + + gotVersions := false + var allRanges []*osvschema.Range + + // Fallback 1: CNA Affected + for _, cveAff := range cve.Containers.CNA.Affected { + versionRanges, _ := w.FindNormalAffectedRanges(cveAff, metrics) + for _, r := range versionRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + } + + if len(allRanges) > 0 { + gotVersions = true + metrics.AddSource(models.VersionSourceAffected) + } + + // Fallback 2: CPE + if !gotVersions { + versionRanges, _ := cpeVersionExtraction(cve, metrics) + for _, r := range versionRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + if len(allRanges) > 0 { + gotVersions = true + } + } + + // Fallback 3: Description + if !gotVersions { + textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) + for _, r := range textRanges { + r.Range.Type = osvschema.Range_ECOSYSTEM + allRanges = append(allRanges, r.Range) + } + if len(allRanges) > 0 { + gotVersions = true + } + } + + if gotVersions { + aff := &osvschema.Affected{ + Package: &osvschema.Package{ + Ecosystem: ecosystem, + Name: slug, + }, + Ranges: allRanges, + } + c.AddAffected(v, aff, metrics) + metrics.Outcome = models.Successful // Override outcome directly + } + } +} + +// DefaultWordpressHandler provides empty implementations for the hooks. +type DefaultWordpressHandler struct{} + +func (d *DefaultWordpressHandler) PreExtract(cve *models.CVE5) {} +func (d *DefaultWordpressHandler) PostExtractDefault(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) {} + +// WordfenceHandler implements Wordfence specific quirks. +type WordfenceHandler struct { + DefaultWordpressHandler +} + +func normalizeVersion(v string) string { + return strings.TrimPrefix(v, "v") +} + +func (w *WordfenceHandler) PreExtract(cve *models.CVE5) { + for i := range cve.Containers.CNA.Affected { + for j := range cve.Containers.CNA.Affected[i].Versions { + vers := &cve.Containers.CNA.Affected[i].Versions[j] + vers.Version = normalizeVersion(vers.Version) + vers.LessThan = normalizeVersion(vers.LessThan) + vers.LessThanOrEqual = normalizeVersion(vers.LessThanOrEqual) + } + } +} + +// PatchstackHandler implements Patchstack specific quirks. +type PatchstackHandler struct { + DefaultWordpressHandler +} + +func (p *PatchstackHandler) PostExtractDefault(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) { + if slug != "" { + var baseURL string + switch ecosystem { + case "WordPress:Plugin": + baseURL = "https://wordpress.org/plugins/" + case "WordPress:Theme": + baseURL = "https://wordpress.org/themes/" + } + + if baseURL != "" { + wpURL := baseURL + slug + "/" + // Check if already exists to avoid duplicates + exists := slices.ContainsFunc(v.References, func(ref *osvschema.Reference) bool { + return ref.GetUrl() == wpURL + }) + if !exists { + v.References = append(v.References, &osvschema.Reference{ + Type: osvschema.Reference_WEB, + Url: wpURL, + }) + metrics.AddNote("Added wordpress.org reference link: %s", wpURL) + } + } + } +} + +// WPScanHandler implements WPScan specific quirks. +type WPScanHandler struct { + DefaultWordpressHandler +} + diff --git a/vulnfeeds/conversion/cve5/wpscan_extractor.go b/vulnfeeds/conversion/cve5/wpscan_extractor.go deleted file mode 100644 index c5bd6c97ddf..00000000000 --- a/vulnfeeds/conversion/cve5/wpscan_extractor.go +++ /dev/null @@ -1,105 +0,0 @@ -package cve5 - -import ( - c "github.com/google/osv/vulnfeeds/conversion" - "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/vulns" - "github.com/ossf/osv-schema/bindings/go/osvschema" -) - -// WPScanVersionExtractor provides version extraction logic for WPScan CVEs. -type WPScanVersionExtractor struct { - DefaultVersionExtractor -} - -var _ VersionExtractor = &WPScanVersionExtractor{} - -// ExtractVersions for WPScanVersionExtractor. -func (w *WPScanVersionExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, repos []string) { - // 1. Run default extraction first - w.DefaultVersionExtractor.ExtractVersions(cve, v, metrics, repos) - - // 2. Extract slug and determine ecosystem using shared helper - slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) - - if slug == "" { - metrics.AddNote("Could not extract slug for WPScan record") - // We might still want to assign the ecosystem if we can determine it - } - - // 3. Update affected packages with correct ecosystem and slug - if len(v.Affected) > 0 { - for _, aff := range v.Affected { - isGit := false - for _, r := range aff.Ranges { - if r.Type == osvschema.Range_GIT { - isGit = true - break - } - } - if isGit { - continue - } - - if aff.Package == nil { - aff.Package = &osvschema.Package{ - Ecosystem: ecosystem, - Name: slug, - } - } else { - // Update ecosystem if it was generic - if aff.Package.Ecosystem == "WordPress" || aff.Package.Ecosystem == "" { - aff.Package.Ecosystem = ecosystem - } - if aff.Package.Name == "" { - aff.Package.Name = slug - } - } - } - } - - // 4. If default extraction didn't produce anything, try to generate ECOSYSTEM ranges. - if len(v.Affected) == 0 { - metrics.AddNote("Attempting to generate ECOSYSTEM ranges for WPScan") - - gotVersions := false - var allRanges []*osvschema.Range - - for _, cveAff := range cve.Containers.CNA.Affected { - versionRanges, _ := w.FindNormalAffectedRanges(cveAff, metrics) - for _, r := range versionRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - } - - if len(allRanges) > 0 { - gotVersions = true - metrics.AddSource(models.VersionSourceAffected) - } - - // Description Fallback - if !gotVersions { - textRanges := c.ExtractVersionsFromText(nil, models.EnglishDescription(cve.Containers.CNA.Descriptions), metrics, models.VersionSourceDescription) - for _, r := range textRanges { - r.Range.Type = osvschema.Range_ECOSYSTEM - allRanges = append(allRanges, r.Range) - } - if len(allRanges) > 0 { - gotVersions = true - } - } - - if gotVersions { - aff := &osvschema.Affected{ - Package: &osvschema.Package{ - Ecosystem: ecosystem, - Name: slug, - }, - Ranges: allRanges, - } - c.AddAffected(v, aff, metrics) - metrics.Outcome = models.Successful - } - } -} From 0b81ec25deb2aab0a1c6e2e354ee8c904ffc77b6 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Thu, 13 Aug 2026 04:58:59 +0000 Subject: [PATCH 07/13] update some wordpress stuff --- vulnfeeds/conversion/cve5/wordpress.go | 52 ++++++++++++++++---------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go index 21e3b0b521c..f4ea81cbec8 100644 --- a/vulnfeeds/conversion/cve5/wordpress.go +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -27,7 +27,7 @@ var ( // the specific WordPress ecosystem (Core, Plugin, Theme) for a given CVE. func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) (string, string) { var slug string - var ecosystem string = "WordPress" // Default/Fallback + var ecosystem = "WordPress" // Default/Fallback // 1. Core Check (Highest Priority) if len(cve.Containers.CNA.Affected) > 0 { @@ -65,25 +65,39 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( if match := wpPluginTracRegex.FindStringSubmatch(url); match != nil { tracSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Plugin" + } } else if match := wpPluginSvnRegex.FindStringSubmatch(url); match != nil { svnSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Plugin" + } } else if match := wordfencePluginRegex.FindStringSubmatch(url); match != nil { wordfenceSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Plugin" + } } else if match := wpPluginOrgRegex.FindStringSubmatch(url); match != nil { wpOrgPluginSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Plugin" + } } else if match := wpThemeOrgRegex.FindStringSubmatch(url); match != nil { wpOrgThemeSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Theme" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Theme" + } } else if match := patchstackPluginRegex.FindStringSubmatch(url); match != nil { patchstackPluginSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Plugin" + } } else if match := patchstackThemeRegex.FindStringSubmatch(url); match != nil { patchstackThemeSlug = match[1] - if urlEcosystem == "" { urlEcosystem = "WordPress:Theme" } + if urlEcosystem == "" { + urlEcosystem = "WordPress:Theme" + } } else if match := patchstackVulnRegex.FindStringSubmatch(url); match != nil { patchstackVulnSlug = match[1] } @@ -153,7 +167,7 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( // WordpressHandler defines hooks for CNA-specific logic. type WordpressHandler interface { PreExtract(cve *models.CVE5) - PostExtractDefault(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) + PostExtractDefault(v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) } // WordpressExtractor handles version extraction for WordPress CVEs. @@ -176,15 +190,15 @@ func (w *WordpressExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerabi slug, ecosystem := extractWordPressSlugAndEcosystem(cve, v) if w.Handler != nil { - w.Handler.PostExtractDefault(cve, v, metrics, slug, ecosystem) + w.Handler.PostExtractDefault(v, metrics, slug, ecosystem) } // 3. Update affected packages with correct ecosystem and slug if len(v.Affected) > 0 { for _, aff := range v.Affected { isGit := false - for _, r := range aff.Ranges { - if r.Type == osvschema.Range_GIT { + for _, r := range aff.GetRanges() { + if r.GetType() == osvschema.Range_GIT { isGit = true break } @@ -198,17 +212,17 @@ func (w *WordpressExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerabi continue // Skip enriching if we have no slug } - if aff.Package == nil { + if aff.GetPackage() == nil { aff.Package = &osvschema.Package{ Ecosystem: ecosystem, Name: slug, } } else { // Update ecosystem if it was generic - if aff.Package.Ecosystem == "WordPress" || aff.Package.Ecosystem == "" { + if aff.GetPackage().GetEcosystem() == "WordPress" || aff.GetPackage().GetEcosystem() == "" { aff.Package.Ecosystem = ecosystem } - if aff.Package.Name == "" { + if aff.GetPackage().GetName() == "" { aff.Package.Name = slug } } @@ -282,8 +296,9 @@ func (w *WordpressExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerabi // DefaultWordpressHandler provides empty implementations for the hooks. type DefaultWordpressHandler struct{} -func (d *DefaultWordpressHandler) PreExtract(cve *models.CVE5) {} -func (d *DefaultWordpressHandler) PostExtractDefault(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) {} +func (d *DefaultWordpressHandler) PreExtract(_ *models.CVE5) {} +func (d *DefaultWordpressHandler) PostExtractDefault(_ *vulns.Vulnerability, _ *models.ConversionMetrics, _ string, _ string) { +} // WordfenceHandler implements Wordfence specific quirks. type WordfenceHandler struct { @@ -310,7 +325,7 @@ type PatchstackHandler struct { DefaultWordpressHandler } -func (p *PatchstackHandler) PostExtractDefault(cve models.CVE5, v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) { +func (p *PatchstackHandler) PostExtractDefault(v *vulns.Vulnerability, metrics *models.ConversionMetrics, slug string, ecosystem string) { if slug != "" { var baseURL string switch ecosystem { @@ -341,4 +356,3 @@ func (p *PatchstackHandler) PostExtractDefault(cve models.CVE5, v *vulns.Vulnera type WPScanHandler struct { DefaultWordpressHandler } - From e7e424f7360f7e822ef72dd9bcaf2f3a60f551db Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 24 Aug 2026 01:28:16 +0000 Subject: [PATCH 08/13] fix lint ? --- vulnfeeds/conversion/cve5/wordpress.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go index f4ea81cbec8..9e68bb53d02 100644 --- a/vulnfeeds/conversion/cve5/wordpress.go +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -5,9 +5,9 @@ import ( "slices" "strings" - c "github.com/google/osv/vulnfeeds/conversion" - "github.com/google/osv/vulnfeeds/models" - "github.com/google/osv/vulnfeeds/vulns" + c "github.com/google/osv.dev/vulnfeeds/conversion" + "github.com/google/osv.dev/vulnfeeds/models" + "github.com/google/osv.dev/vulnfeeds/vulns" "github.com/ossf/osv-schema/bindings/go/osvschema" ) @@ -173,6 +173,7 @@ type WordpressHandler interface { // WordpressExtractor handles version extraction for WordPress CVEs. type WordpressExtractor struct { DefaultVersionExtractor + Handler WordpressHandler } From 93676d1bb7a87d3bb7a63534673c199df0b0aa5e Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 24 Aug 2026 01:44:55 +0000 Subject: [PATCH 09/13] add test file --- .../cves/2026/1xxx/CVE-2026-1293.json | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 vulnfeeds/test_data/cvelistV5/cves/2026/1xxx/CVE-2026-1293.json diff --git a/vulnfeeds/test_data/cvelistV5/cves/2026/1xxx/CVE-2026-1293.json b/vulnfeeds/test_data/cvelistV5/cves/2026/1xxx/CVE-2026-1293.json new file mode 100644 index 00000000000..31acf3a7fdf --- /dev/null +++ b/vulnfeeds/test_data/cvelistV5/cves/2026/1xxx/CVE-2026-1293.json @@ -0,0 +1,133 @@ +{ + "dataType": "CVE_RECORD", + "dataVersion": "5.2", + "cveMetadata": { + "cveId": "CVE-2026-1293", + "assignerOrgId": "b15e7b5b-3da4-40ae-a43c-f7aa60e62599", + "state": "PUBLISHED", + "assignerShortName": "Wordfence", + "dateReserved": "2026-01-21T16:53:09.134Z", + "datePublished": "2026-02-06T11:21:30.973Z", + "dateUpdated": "2026-04-08T17:05:48.147Z" + }, + "containers": { + "cna": { + "providerMetadata": { + "orgId": "b15e7b5b-3da4-40ae-a43c-f7aa60e62599", + "shortName": "Wordfence", + "dateUpdated": "2026-04-08T17:05:48.147Z" + }, + "affected": [ + { + "vendor": "yoast", + "product": "Yoast SEO – Advanced SEO with real-time guidance and built-in AI", + "versions": [ + { + "version": "0", + "status": "affected", + "lessThanOrEqual": "26.8", + "versionType": "semver" + } + ], + "defaultStatus": "unaffected" + } + ], + "descriptions": [ + { + "lang": "en", + "value": "The Yoast SEO – Advanced SEO with real-time guidance and built-in AI plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the the `yoast-schema` block attribute in all versions up to, and including, 26.8 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page." + } + ], + "title": "Yoast SEO <= 26.8 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'yoast-schema' Block Attribute", + "references": [ + { + "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/8b2e7c2d-ed2f-439b-9cee-f2e5d46121b6?source=cve" + }, + { + "url": "https://plugins.trac.wordpress.org/browser/wordpress-seo/tags/26.8/src/presenters/schema-presenter.php#L49" + }, + { + "url": "https://plugins.trac.wordpress.org/browser/wordpress-seo/tags/26.8/inc/class-wpseo-utils.php#L915" + }, + { + "url": "https://plugins.trac.wordpress.org/browser/wordpress-seo/tags/26.8/src/generators/schema-generator.php#L188" + } + ], + "problemTypes": [ + { + "descriptions": [ + { + "lang": "en", + "description": "CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", + "cweId": "CWE-79", + "type": "CWE" + } + ] + } + ], + "metrics": [ + { + "cvssV3_1": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", + "baseScore": 6.4, + "baseSeverity": "MEDIUM" + } + } + ], + "credits": [ + { + "lang": "en", + "type": "finder", + "value": "suyoung kim" + } + ], + "timeline": [ + { + "time": "2026-01-09T00:00:00.000Z", + "lang": "en", + "value": "Discovered" + }, + { + "time": "2026-02-05T22:18:00.000Z", + "lang": "en", + "value": "Disclosed" + } + ] + }, + "adp": [ + { + "metrics": [ + { + "other": { + "type": "ssvc", + "content": { + "timestamp": "2026-02-06T12:27:02.594220Z", + "id": "CVE-2026-1293", + "options": [ + { + "Exploitation": "none" + }, + { + "Automatable": "no" + }, + { + "Technical Impact": "partial" + } + ], + "role": "CISA Coordinator", + "version": "2.0.3" + } + } + } + ], + "title": "CISA ADP Vulnrichment", + "providerMetadata": { + "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0", + "shortName": "CISA-ADP", + "dateUpdated": "2026-02-06T12:27:32.895Z" + } + } + ] + } +} \ No newline at end of file From c5a908cc8f60e3ac69b87e6e9f07917684dfce3e Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 24 Aug 2026 03:11:52 +0000 Subject: [PATCH 10/13] only use refs to extract slugs - may need to be revised on cna case-by-case --- vulnfeeds/conversion/common.go | 15 ++++- .../cve5/version_extraction_test.go | 22 ++----- vulnfeeds/conversion/cve5/wordpress.go | 61 +++++++++---------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/vulnfeeds/conversion/common.go b/vulnfeeds/conversion/common.go index 3c58edbead8..250424ccf1f 100644 --- a/vulnfeeds/conversion/common.go +++ b/vulnfeeds/conversion/common.go @@ -743,7 +743,20 @@ func ProcessRanges(ranges []models.RangeWithMetadata, repos []string, metrics *m if len(un) > 0 { metrics.UnresolvedRangesCount += len(un) if len(r) == 0 { - metrics.SetOutcome(models.NoCommitRanges) + hasRepo := len(repos) > 0 + if !hasRepo { + for _, ra := range ranges { + if ra.Range.GetRepo() != "" { + hasRepo = true + break + } + } + } + if !hasRepo { + metrics.SetOutcome(models.NoRepos) + } else { + metrics.SetOutcome(models.NoCommitRanges) + } } } diff --git a/vulnfeeds/conversion/cve5/version_extraction_test.go b/vulnfeeds/conversion/cve5/version_extraction_test.go index 12356e46fcb..622331f11ff 100644 --- a/vulnfeeds/conversion/cve5/version_extraction_test.go +++ b/vulnfeeds/conversion/cve5/version_extraction_test.go @@ -661,23 +661,11 @@ func TestExtractVersions(t *testing.T) { }}, }, { - name: "CVE-2015-10001", - cve: loadTestData(t, "CVE-2015-10001"), - cnaAssigner: "WPScan", - repos: []string{}, - expectedAffected: []*osvschema.Affected{{ - Package: &osvschema.Package{ - Ecosystem: "WordPress:Plugin", - Name: "wp-stats", - }, - Ranges: []*osvschema.Range{{ - Type: osvschema.Range_ECOSYSTEM, - Events: []*osvschema.Event{ - {Introduced: "2.52"}, - {LastAffected: "2.52"}, - }, - }}, - }}, + name: "CVE-2015-10001", + cve: loadTestData(t, "CVE-2015-10001"), + cnaAssigner: "WPScan", + repos: []string{}, + expectedAffected: nil, }, } diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go index 9e68bb53d02..e6bf505d53b 100644 --- a/vulnfeeds/conversion/cve5/wordpress.go +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -12,11 +12,15 @@ import ( ) var ( - wpPluginTracRegex = regexp.MustCompile(`plugins\.trac\.wordpress\.org/browser/([^/]+)`) - wpPluginSvnRegex = regexp.MustCompile(`plugins\.svn\.wordpress\.org/([^/]+)`) + wpPluginTracRegex = regexp.MustCompile(`plugins\.trac\.wordpress\.org/browser/([^/]+)`) + wpPluginSvnRegex = regexp.MustCompile(`plugins\.svn\.wordpress\.org/([^/]+)`) + wpPluginOrgRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) + wpThemeTracRegex = regexp.MustCompile(`themes\.trac\.wordpress\.org/browser/([^/]+)`) + wpThemeSvnRegex = regexp.MustCompile(`themes\.svn\.wordpress\.org/([^/]+)`) + wpThemeOrgRegex = regexp.MustCompile(`wordpress\.org/themes/([^/]+)`) + wordfencePluginRegex = regexp.MustCompile(`wordfence\.com/threat-intel/vulnerabilities/wordpress-plugins/([^/]+)`) - wpPluginOrgRegex = regexp.MustCompile(`wordpress\.org/plugins/([^/]+)`) - wpThemeOrgRegex = regexp.MustCompile(`wordpress\.org/themes/([^/]+)`) + wordfenceThemeRegex = regexp.MustCompile(`wordfence\.com/threat-intel/vulnerabilities/wordpress-themes/([^/]+)`) patchstackVulnRegex = regexp.MustCompile(`patchstack\.com/database/vulnerability/([^/]+)`) patchstackPluginRegex = regexp.MustCompile(`patchstack\.com/database/wordpress/plugin/([^/]+)`) @@ -37,15 +41,7 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( } } - // 2. Slug from PackageName - if len(cve.Containers.CNA.Affected) > 0 { - aff := cve.Containers.CNA.Affected[0] - if aff.PackageName != "" { - slug = aff.PackageName - } - } - - // 3. Ecosystem Extraction from CollectionURL + // 2. Ecosystem Extraction from CollectionURL if len(cve.Containers.CNA.Affected) > 0 { aff := cve.Containers.CNA.Affected[0] switch aff.CollectionURL { @@ -56,7 +52,7 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( } } - // 4. URL Heuristics Fallback (for slug and ecosystem) + // 3. Extract slug and ecosystem from Reference URLs var tracSlug, svnSlug, wordfenceSlug, wpOrgPluginSlug, wpOrgThemeSlug, patchstackPluginSlug, patchstackThemeSlug, patchstackVulnSlug string var urlEcosystem string @@ -73,11 +69,26 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := wpThemeTracRegex.FindStringSubmatch(url); match != nil { + tracSlug = match[1] + if urlEcosystem == "" { + urlEcosystem = "WordPress:Theme" + } + } else if match := wpThemeSvnRegex.FindStringSubmatch(url); match != nil { + svnSlug = match[1] + if urlEcosystem == "" { + urlEcosystem = "WordPress:Theme" + } } else if match := wordfencePluginRegex.FindStringSubmatch(url); match != nil { wordfenceSlug = match[1] if urlEcosystem == "" { urlEcosystem = "WordPress:Plugin" } + } else if match := wordfenceThemeRegex.FindStringSubmatch(url); match != nil { + wordfenceSlug = match[1] + if urlEcosystem == "" { + urlEcosystem = "WordPress:Theme" + } } else if match := wpPluginOrgRegex.FindStringSubmatch(url); match != nil { wpOrgPluginSlug = match[1] if urlEcosystem == "" { @@ -112,7 +123,6 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( } } - var urlSlug string slugsToTry := []string{ tracSlug, svnSlug, @@ -126,30 +136,16 @@ func extractWordPressSlugAndEcosystem(cve models.CVE5, v *vulns.Vulnerability) ( for _, s := range slugsToTry { if s != "" { - urlSlug = s + slug = s break } } - if slug == "" && urlSlug != "" { - slug = urlSlug - } - - // Slug from Product Fallback - if slug == "" && len(cve.Containers.CNA.Affected) > 0 { - aff := cve.Containers.CNA.Affected[0] - if vulns.CheckQuality(aff.Product).AtLeast(vulns.Spaces) { - // Basic slugification (lowercase, replace spaces with hyphens) - slug = strings.ToLower(aff.Product) - slug = strings.ReplaceAll(slug, " ", "-") - } - } - if ecosystem == "WordPress" && urlEcosystem != "" { ecosystem = urlEcosystem } - // 5. Description/Title Heuristics Fallback + // 4. Description/Title Heuristics Fallback for ecosystem if ecosystem == "WordPress" { desc := strings.ToLower(models.EnglishDescription(cve.Containers.CNA.Descriptions)) title := strings.ToLower(cve.Containers.CNA.Title) @@ -234,6 +230,9 @@ func (w *WordpressExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerabi if len(v.Affected) == 0 { if slug == "" { metrics.AddNote("Failed to extract versions via default, and no WordPress slug found to attempt fallback") + if len(repos) == 0 { + metrics.Outcome = models.NoRepos + } return } From 0c6110558e62205f218bab3fc0f6f785c64f5cf4 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 24 Aug 2026 03:22:22 +0000 Subject: [PATCH 11/13] fix lint --- vulnfeeds/conversion/cve5/wordpress.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vulnfeeds/conversion/cve5/wordpress.go b/vulnfeeds/conversion/cve5/wordpress.go index e6bf505d53b..22a5cad1507 100644 --- a/vulnfeeds/conversion/cve5/wordpress.go +++ b/vulnfeeds/conversion/cve5/wordpress.go @@ -233,6 +233,7 @@ func (w *WordpressExtractor) ExtractVersions(cve models.CVE5, v *vulns.Vulnerabi if len(repos) == 0 { metrics.Outcome = models.NoRepos } + return } From 5c1fcc0bff24776c496f38c540f89cc440c231d8 Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 24 Aug 2026 05:41:30 +0000 Subject: [PATCH 12/13] testcase --- .../cves/2021/23xxx/CVE-2021-23209.json | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 vulnfeeds/test_data/cvelistV5/cves/2021/23xxx/CVE-2021-23209.json diff --git a/vulnfeeds/test_data/cvelistV5/cves/2021/23xxx/CVE-2021-23209.json b/vulnfeeds/test_data/cvelistV5/cves/2021/23xxx/CVE-2021-23209.json new file mode 100644 index 00000000000..b7358d5cb5e --- /dev/null +++ b/vulnfeeds/test_data/cvelistV5/cves/2021/23xxx/CVE-2021-23209.json @@ -0,0 +1,299 @@ +{ + "containers": { + "cna": { + "affected": [ + { + "collectionURL": "https://wordpress.org/plugins", + "defaultStatus": "unaffected", + "packageName": "accelerated-mobile-pages", + "product": "AMP for WP – Accelerated Mobile Pages (WordPress plugin)", + "vendor": "Ahmed Kaludi, Mohammed Kaludi", + "versions": [ + { + "changes": [ + { + "at": "1.0.77.33", + "status": "unaffected" + } + ], + "lessThanOrEqual": "1.0.77.32", + "status": "affected", + "version": "n/a", + "versionType": "custom" + } + ] + } + ], + "credits": [ + { + "lang": "en", + "type": "finder", + "user": "00000000-0000-4000-9000-000000000000", + "value": "FearZzZz (Patchstack Alliance)" + } + ], + "datePublic": "2021-12-14T17:00:00.000Z", + "descriptions": [ + { + "lang": "en", + "supportingMedia": [ + { + "base64": false, + "type": "text/html", + "value": "

Multiple Authenticated (admin user role) Persistent Cross-Site Scripting (XSS) vulnerabilities discovered in AMP for WP – Accelerated Mobile Pages WordPress plugin (versions <= 1.0.77.32).

" + } + ], + "value": "Multiple Authenticated (admin user role) Persistent Cross-Site Scripting (XSS) vulnerabilities discovered in AMP for WP – Accelerated Mobile Pages WordPress plugin (versions <= 1.0.77.32)." + } + ], + "impacts": [ + { + "capecId": "CAPEC-592", + "descriptions": [ + { + "lang": "en", + "value": "CAPEC-592 Stored XSS" + } + ] + } + ], + "metrics": [ + { + "cvssV3_1": { + "attackComplexity": "LOW", + "attackVector": "NETWORK", + "availabilityImpact": "NONE", + "baseScore": 4.8, + "baseSeverity": "MEDIUM", + "confidentialityImpact": "LOW", + "integrityImpact": "LOW", + "privilegesRequired": "HIGH", + "scope": "CHANGED", + "userInteraction": "REQUIRED", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N", + "version": "3.1" + }, + "format": "CVSS", + "scenarios": [ + { + "lang": "en", + "value": "GENERAL" + } + ] + } + ], + "problemTypes": [ + { + "descriptions": [ + { + "cweId": "CWE-79", + "description": "CWE-79 Cross-site Scripting (XSS)", + "lang": "en", + "type": "CWE" + } + ] + } + ], + "providerMetadata": { + "orgId": "21595511-bba5-4825-b968-b78d1f9984a3", + "shortName": "Patchstack", + "dateUpdated": "2026-04-28T16:07:30.318Z" + }, + "references": [ + { + "tags": [ + "vdb-entry" + ], + "url": "https://patchstack.com/database/vulnerability/accelerated-mobile-pages/wordpress-amp-for-wp-accelerated-mobile-pages-plugin-1-0-77-32-multiple-authenticated-persistent-cross-site-scripting-xss-vulnerabilities?_s_id=cve" + } + ], + "solutions": [ + { + "lang": "en", + "supportingMedia": [ + { + "base64": false, + "type": "text/html", + "value": "

Update to 1.0.77.33 or higher version.

" + } + ], + "value": "Update to 1.0.77.33 or higher version." + } + ], + "source": { + "discovery": "EXTERNAL" + }, + "title": "WordPress AMP for WP – Accelerated Mobile Pages plugin <= 1.0.77.32 - Multiple Auth. Stored Cross-Site Scripting (XSS) vulnerabilities", + "x_generator": { + "engine": "Vulnogram 0.0.9" + }, + "x_legacyV4Record": { + "CVE_data_meta": { + "ASSIGNER": "audit@patchstack.com", + "DATE_PUBLIC": "2021-12-15T10:11:00.000Z", + "ID": "CVE-2021-23209", + "STATE": "PUBLIC", + "TITLE": "WordPress AMP for WP – Accelerated Mobile Pages plugin <= 1.0.77.32 - Multiple Authenticated Persistent Cross-Site Scripting (XSS) vulnerabilities" + }, + "affects": { + "vendor": { + "vendor_data": [ + { + "product": { + "product_data": [ + { + "product_name": "AMP for WP – Accelerated Mobile Pages (WordPress plugin)", + "version": { + "version_data": [ + { + "version_affected": "<=", + "version_name": "<= 1.0.77.32", + "version_value": "1.0.77.32" + } + ] + } + } + ] + }, + "vendor_name": "Ahmed Kaludi, Mohammed Kaludi" + } + ] + } + }, + "credit": [ + { + "lang": "eng", + "value": "Vulnerability discovered by Ex.Mi (Patchstack)." + } + ], + "data_format": "MITRE", + "data_type": "CVE", + "data_version": "4.0", + "description": { + "description_data": [ + { + "lang": "eng", + "value": "Multiple Authenticated (admin user role) Persistent Cross-Site Scripting (XSS) vulnerabilities discovered in AMP for WP – Accelerated Mobile Pages WordPress plugin (versions <= 1.0.77.32)." + } + ] + }, + "generator": { + "engine": "Vulnogram 0.0.9" + }, + "impact": { + "cvss": { + "attackComplexity": "LOW", + "attackVector": "NETWORK", + "availabilityImpact": "NONE", + "baseScore": 4.8, + "baseSeverity": "MEDIUM", + "confidentialityImpact": "LOW", + "integrityImpact": "LOW", + "privilegesRequired": "HIGH", + "scope": "CHANGED", + "userInteraction": "REQUIRED", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N", + "version": "3.1" + } + }, + "problemtype": { + "problemtype_data": [ + { + "description": [ + { + "lang": "eng", + "value": "CWE-79 Cross-site Scripting (XSS)" + } + ] + } + ] + }, + "references": { + "reference_data": [ + { + "name": "https://wordpress.org/plugins/accelerated-mobile-pages/#developers", + "refsource": "CONFIRM", + "url": "https://wordpress.org/plugins/accelerated-mobile-pages/#developers" + }, + { + "name": "https://patchstack.com/database/vulnerability/accelerated-mobile-pages/wordpress-amp-for-wp-accelerated-mobile-pages-plugin-1-0-77-32-multiple-authenticated-persistent-cross-site-scripting-xss-vulnerabilities", + "refsource": "CONFIRM", + "url": "https://patchstack.com/database/vulnerability/accelerated-mobile-pages/wordpress-amp-for-wp-accelerated-mobile-pages-plugin-1-0-77-32-multiple-authenticated-persistent-cross-site-scripting-xss-vulnerabilities" + } + ] + }, + "solution": [ + { + "lang": "en", + "value": "Update to 1.0.77.33 or higher version." + } + ], + "source": { + "discovery": "EXTERNAL" + } + } + }, + "adp": [ + { + "providerMetadata": { + "orgId": "af854a3a-2127-422b-91ae-364da2661108", + "shortName": "CVE", + "dateUpdated": "2024-08-03T19:05:55.659Z" + }, + "title": "CVE Program Container", + "references": [ + { + "tags": [ + "vdb-entry", + "x_transferred" + ], + "url": "https://patchstack.com/database/vulnerability/accelerated-mobile-pages/wordpress-amp-for-wp-accelerated-mobile-pages-plugin-1-0-77-32-multiple-authenticated-persistent-cross-site-scripting-xss-vulnerabilities?_s_id=cve" + } + ] + }, + { + "metrics": [ + { + "other": { + "type": "ssvc", + "content": { + "timestamp": "2025-04-23T13:06:51.972194Z", + "id": "CVE-2021-23209", + "options": [ + { + "Exploitation": "none" + }, + { + "Automatable": "no" + }, + { + "Technical Impact": "partial" + } + ], + "role": "CISA Coordinator", + "version": "2.0.3" + } + } + } + ], + "title": "CISA ADP Vulnrichment", + "providerMetadata": { + "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0", + "shortName": "CISA-ADP", + "dateUpdated": "2025-04-23T18:45:39.935Z" + } + } + ] + }, + "cveMetadata": { + "assignerOrgId": "21595511-bba5-4825-b968-b78d1f9984a3", + "assignerShortName": "Patchstack", + "cveId": "CVE-2021-23209", + "datePublished": "2022-03-18T18:00:25.146Z", + "dateReserved": "2022-01-13T00:00:00.000Z", + "dateUpdated": "2026-04-28T16:07:30.318Z", + "state": "PUBLISHED" + }, + "dataType": "CVE_RECORD", + "dataVersion": "5.2" +} \ No newline at end of file From 91e835e3ce4a2d052635a9e17b57d9df085e605c Mon Sep 17 00:00:00 2001 From: Jess Lowe Date: Mon, 24 Aug 2026 06:09:18 +0000 Subject: [PATCH 13/13] more missing testdata --- .../cves/2015/10xxx/CVE-2015-10001.json | 185 ++++++++++++++++++ .../cves/2024/27xxx/CVE-2024-27098.json | 166 ++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 vulnfeeds/test_data/cvelistV5/cves/2015/10xxx/CVE-2015-10001.json create mode 100644 vulnfeeds/test_data/cvelistV5/cves/2024/27xxx/CVE-2024-27098.json diff --git a/vulnfeeds/test_data/cvelistV5/cves/2015/10xxx/CVE-2015-10001.json b/vulnfeeds/test_data/cvelistV5/cves/2015/10xxx/CVE-2015-10001.json new file mode 100644 index 00000000000..400644c2eab --- /dev/null +++ b/vulnfeeds/test_data/cvelistV5/cves/2015/10xxx/CVE-2015-10001.json @@ -0,0 +1,185 @@ +{ + "containers": { + "cna": { + "affected": [ + { + "product": "WP-Stats", + "vendor": "Unknown", + "versions": [ + { + "lessThan": "2.52", + "status": "affected", + "version": "2.52", + "versionType": "custom" + } + ] + } + ], + "credits": [ + { + "lang": "en", + "value": "Sebastian Wolfgang Kraemer" + } + ], + "descriptions": [ + { + "lang": "en", + "value": "The WP-Stats WordPress plugin before 2.52 does not have CSRF check when saving its settings, and did not escape some of them when outputting them, allowing attacker to make logged in high privilege users change them and set Cross-Site Scripting payloads" + } + ], + "problemTypes": [ + { + "descriptions": [ + { + "cweId": "CWE-352", + "description": "CWE-352 Cross-Site Request Forgery (CSRF)", + "lang": "en", + "type": "CWE" + } + ] + } + ], + "providerMetadata": { + "dateUpdated": "2021-11-01T08:45:47.000Z", + "orgId": "1bfdd5d7-9bf6-4a53-96ea-42e2716d7a81", + "shortName": "WPScan" + }, + "references": [ + { + "tags": [ + "x_refsource_MISC" + ], + "url": "https://wpscan.com/vulnerability/f5c3dfea-7203-4a98-88ff-aa6a24d03734" + }, + { + "tags": [ + "x_refsource_MISC" + ], + "url": "https://www.openwall.com/lists/oss-security/2015/06/17/6" + } + ], + "source": { + "discovery": "EXTERNAL" + }, + "title": "WP-Stats < 2.5.2 - CSRF to Stored Cross-Site Scripting (XSS)", + "x_generator": "WPScan CVE Generator", + "x_legacyV4Record": { + "CVE_data_meta": { + "ASSIGNER": "contact@wpscan.com", + "ID": "CVE-2015-10001", + "STATE": "PUBLIC", + "TITLE": "WP-Stats < 2.5.2 - CSRF to Stored Cross-Site Scripting (XSS)" + }, + "affects": { + "vendor": { + "vendor_data": [ + { + "product": { + "product_data": [ + { + "product_name": "WP-Stats", + "version": { + "version_data": [ + { + "version_affected": "<", + "version_name": "2.52", + "version_value": "2.52" + } + ] + } + } + ] + }, + "vendor_name": "Unknown" + } + ] + } + }, + "credit": [ + { + "lang": "eng", + "value": "Sebastian Wolfgang Kraemer" + } + ], + "data_format": "MITRE", + "data_type": "CVE", + "data_version": "4.0", + "description": { + "description_data": [ + { + "lang": "eng", + "value": "The WP-Stats WordPress plugin before 2.52 does not have CSRF check when saving its settings, and did not escape some of them when outputting them, allowing attacker to make logged in high privilege users change them and set Cross-Site Scripting payloads" + } + ] + }, + "generator": "WPScan CVE Generator", + "problemtype": { + "problemtype_data": [ + { + "description": [ + { + "lang": "eng", + "value": "CWE-352 Cross-Site Request Forgery (CSRF)" + } + ] + } + ] + }, + "references": { + "reference_data": [ + { + "name": "https://wpscan.com/vulnerability/f5c3dfea-7203-4a98-88ff-aa6a24d03734", + "refsource": "MISC", + "url": "https://wpscan.com/vulnerability/f5c3dfea-7203-4a98-88ff-aa6a24d03734" + }, + { + "name": "https://www.openwall.com/lists/oss-security/2015/06/17/6", + "refsource": "MISC", + "url": "https://www.openwall.com/lists/oss-security/2015/06/17/6" + } + ] + }, + "source": { + "discovery": "EXTERNAL" + } + } + }, + "adp": [ + { + "providerMetadata": { + "orgId": "af854a3a-2127-422b-91ae-364da2661108", + "shortName": "CVE", + "dateUpdated": "2024-08-06T08:58:24.557Z" + }, + "title": "CVE Program Container", + "references": [ + { + "tags": [ + "x_refsource_MISC", + "x_transferred" + ], + "url": "https://wpscan.com/vulnerability/f5c3dfea-7203-4a98-88ff-aa6a24d03734" + }, + { + "tags": [ + "x_refsource_MISC", + "x_transferred" + ], + "url": "https://www.openwall.com/lists/oss-security/2015/06/17/6" + } + ] + } + ] + }, + "cveMetadata": { + "assignerOrgId": "1bfdd5d7-9bf6-4a53-96ea-42e2716d7a81", + "assignerShortName": "WPScan", + "cveId": "CVE-2015-10001", + "datePublished": "2021-11-01T08:45:47.000Z", + "dateReserved": "2021-10-31T00:00:00.000Z", + "dateUpdated": "2024-08-06T08:58:24.557Z", + "state": "PUBLISHED" + }, + "dataType": "CVE_RECORD", + "dataVersion": "5.1" +} \ No newline at end of file diff --git a/vulnfeeds/test_data/cvelistV5/cves/2024/27xxx/CVE-2024-27098.json b/vulnfeeds/test_data/cvelistV5/cves/2024/27xxx/CVE-2024-27098.json new file mode 100644 index 00000000000..1426239e54f --- /dev/null +++ b/vulnfeeds/test_data/cvelistV5/cves/2024/27xxx/CVE-2024-27098.json @@ -0,0 +1,166 @@ +{ + "dataType": "CVE_RECORD", + "dataVersion": "5.1", + "cveMetadata": { + "cveId": "CVE-2024-27098", + "assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa", + "state": "PUBLISHED", + "assignerShortName": "GitHub_M", + "dateReserved": "2024-02-19T14:43:05.993Z", + "datePublished": "2024-03-18T16:14:18.894Z", + "dateUpdated": "2024-08-02T00:27:59.073Z" + }, + "containers": { + "cna": { + "title": "Blind Server-Side Request Forgery (SSRF) using Arbitrary Object Instantiation in GLPI", + "problemTypes": [ + { + "descriptions": [ + { + "cweId": "CWE-918", + "lang": "en", + "description": "CWE-918: Server-Side Request Forgery (SSRF)", + "type": "CWE" + } + ] + } + ], + "metrics": [ + { + "cvssV3_1": { + "attackComplexity": "LOW", + "attackVector": "NETWORK", + "availabilityImpact": "NONE", + "baseScore": 6.4, + "baseSeverity": "MEDIUM", + "confidentialityImpact": "LOW", + "integrityImpact": "LOW", + "privilegesRequired": "LOW", + "scope": "CHANGED", + "userInteraction": "NONE", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", + "version": "3.1" + } + } + ], + "references": [ + { + "name": "https://github.com/glpi-project/glpi/security/advisories/GHSA-92x4-q9w5-837w", + "tags": [ + "x_refsource_CONFIRM" + ], + "url": "https://github.com/glpi-project/glpi/security/advisories/GHSA-92x4-q9w5-837w" + }, + { + "name": "https://github.com/glpi-project/glpi/commit/3b6bc1b4aa1f3693b20ada3425d2de5108522484", + "tags": [ + "x_refsource_MISC" + ], + "url": "https://github.com/glpi-project/glpi/commit/3b6bc1b4aa1f3693b20ada3425d2de5108522484" + }, + { + "name": "https://github.com/glpi-project/glpi/releases/tag/10.0.13", + "tags": [ + "x_refsource_MISC" + ], + "url": "https://github.com/glpi-project/glpi/releases/tag/10.0.13" + } + ], + "affected": [ + { + "vendor": "glpi-project", + "product": "glpi", + "versions": [ + { + "version": ">= 9.5.0, < 10.0.13", + "status": "affected" + } + ] + } + ], + "providerMetadata": { + "orgId": "a0819718-46f1-4df5-94e2-005712e83aaa", + "shortName": "GitHub_M", + "dateUpdated": "2024-03-18T16:14:18.894Z" + }, + "descriptions": [ + { + "lang": "en", + "value": "GLPI is a Free Asset and IT Management Software package, Data center management, ITIL Service Desk, licenses tracking and software auditing. An authenticated user can execute a SSRF based attack using Arbitrary Object Instantiation. This issue has been patched in version 10.0.13." + } + ], + "source": { + "advisory": "GHSA-92x4-q9w5-837w", + "discovery": "UNKNOWN" + } + }, + "adp": [ + { + "title": "CISA ADP Vulnrichment", + "metrics": [ + { + "other": { + "type": "ssvc", + "content": { + "id": "CVE-2024-27098", + "role": "CISA Coordinator", + "options": [ + { + "Exploitation": "none" + }, + { + "Automatable": "no" + }, + { + "Technical Impact": "partial" + } + ], + "version": "2.0.3", + "timestamp": "2024-03-19T15:47:43.513847Z" + } + } + } + ], + "providerMetadata": { + "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0", + "shortName": "CISA-ADP", + "dateUpdated": "2024-06-04T17:47:14.574Z" + } + }, + { + "providerMetadata": { + "orgId": "af854a3a-2127-422b-91ae-364da2661108", + "shortName": "CVE", + "dateUpdated": "2024-08-02T00:27:59.073Z" + }, + "title": "CVE Program Container", + "references": [ + { + "name": "https://github.com/glpi-project/glpi/security/advisories/GHSA-92x4-q9w5-837w", + "tags": [ + "x_refsource_CONFIRM", + "x_transferred" + ], + "url": "https://github.com/glpi-project/glpi/security/advisories/GHSA-92x4-q9w5-837w" + }, + { + "name": "https://github.com/glpi-project/glpi/commit/3b6bc1b4aa1f3693b20ada3425d2de5108522484", + "tags": [ + "x_refsource_MISC", + "x_transferred" + ], + "url": "https://github.com/glpi-project/glpi/commit/3b6bc1b4aa1f3693b20ada3425d2de5108522484" + }, + { + "name": "https://github.com/glpi-project/glpi/releases/tag/10.0.13", + "tags": [ + "x_refsource_MISC", + "x_transferred" + ], + "url": "https://github.com/glpi-project/glpi/releases/tag/10.0.13" + } + ] + } + ] + } +} \ No newline at end of file