diff --git a/articles/vulnerability-processing.md b/articles/vulnerability-processing.md index 05f96db6e4..c534385259 100644 --- a/articles/vulnerability-processing.md +++ b/articles/vulnerability-processing.md @@ -30,12 +30,15 @@ If you find that Fleet is incorrectly marking software as vulnerable (false posi ## Sources Fleet combines multiple sources to get accurate and up-to-date CVE information: -- [National Vulnerability Database](https://nvd.nist.gov/developers/vulnerabilities) CVE feeds and CVSS scores from primary sources (available on Fleet Premium). +- [National Vulnerability Database](https://nvd.nist.gov/developers/vulnerabilities) CVE feeds - [VulnCheck](https://vulncheck.com/) Enriched NVD CPE data - [Mac Office release notes](https://learn.microsoft.com/en-us/officeupdates/release-notes-office-for-mac) Office for Mac vulnerabilities - [Microsoft MSRC Security Bulletins](https://msrc.microsoft.com/update-guide) for Windows OS vulnerabilities - [OVAL definitions](https://github.com/fleetdm/nvd/blob/master/oval_sources.json) for Linux software +> Fleet Premium includes CVSSv3 scores from NVD CVE feeds. Primary scores are preferred to Secondary scores +> if both are available, and v3.1 scores of the same type are preferred to v3.0. + ## Advanced configuration Fleet runs vulnerability downloading and processing via internal scheduled cron job. This internal mechanism is very useful diff --git a/changes/28261-cvss-fallback b/changes/28261-cvss-fallback new file mode 100644 index 0000000000..c0b86fb993 --- /dev/null +++ b/changes/28261-cvss-fallback @@ -0,0 +1 @@ +* Update vulnerabilities feed to fall back to non-primary CVSSv2/v3 sources when primary (NVD) data is not available, instead of omitting scores entirely. diff --git a/cmd/cve/validate/main.go b/cmd/cve/validate/main.go index 524a9f33fe..a1d5985427 100644 --- a/cmd/cve/validate/main.go +++ b/cmd/cve/validate/main.go @@ -41,12 +41,12 @@ func checkNVDVulnerabilities(vulnPath string, logger log.Logger) { panic(err) } - // make sure VulnCheck enrichment is working vulns, err := cvefeed.LoadJSONDictionary(filepath.Join(vulnPath, "nvdcve-1.1-2025.json.gz")) if err != nil { panic(err) } + // make sure VulnCheck enrichment is working vulnEntry, ok := vulns["CVE-2025-0938"].(*feednvd.Vuln) if !ok { panic("failed to cast CVE-2025-0938 to a Vuln") @@ -55,11 +55,17 @@ func checkNVDVulnerabilities(vulnPath string, logger log.Logger) { panic(errors.New("enriched vulnerability spot-check failed for Python on CVE-2025-0938")) } + // check CVSS score extraction; confirm that secondary CVSS scores are extracted when primary isn't set + if vulns["CVE-2025-3196"].CVSSv3BaseScore() != 5.3 { // CVSSv4 score and CVSSv3 score are different + panic(errors.New("cvss v3 spot-check failed for CVE-2025-3196")) + } + vulns, err = cvefeed.LoadJSONDictionary(filepath.Join(vulnPath, "nvdcve-1.1-2024.json.gz")) if err != nil { panic(err) } + // make sure VulnCheck enrichment is working on less recent vulns vulnEntry, ok = vulns["CVE-2024-6286"].(*feednvd.Vuln) if !ok { panic("failed to cast CVE-2024-6286 to a Vuln") @@ -68,6 +74,16 @@ func checkNVDVulnerabilities(vulnPath string, logger log.Logger) { vulnEntry.Schema().Configurations.Nodes[0].CPEMatch[1].VersionEndExcluding != "2403.1" { panic(errors.New("enriched vulnerability spot-check failed for Citrix Workstation on CVE-2024-6286")) } + + if vulns["CVE-2024-54559"].CVSSv3BaseScore() != 5.5 { // secondary source CVSS score + panic(errors.New("cvss v3 spot-check failed for CVE-2024-54559")) + } + if vulns["CVE-2024-0450"].CVSSv3BaseScore() != 6.2 { // secondary source CVSS score + panic(errors.New("cvss v3 spot-check failed for CVE-2024-0450")) + } + if vulns["CVE-2024-0540"].CVSSv3BaseScore() != 9.8 { // primary source CVSS score + panic(errors.New("cvss v3 spot-check failed for CVE-2024-0540")) + } } func checkGovalDictionaryVulnerabilities(vulnPath string) { diff --git a/server/vulnerabilities/nvd/sync/cve_syncer.go b/server/vulnerabilities/nvd/sync/cve_syncer.go index 859ddca017..696d3adaca 100644 --- a/server/vulnerabilities/nvd/sync/cve_syncer.go +++ b/server/vulnerabilities/nvd/sync/cve_syncer.go @@ -16,6 +16,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -910,10 +911,18 @@ func convertAPI20CVEToLegacy(cve nvdapi.CVE, logger log.Logger) *schema.NVDCVEFe } var baseMetricV2 *schema.NVDCVEFeedJSON10DefImpactBaseMetricV2 - for _, cvssMetricV2 := range cve.Metrics.CVSSMetricV2 { - if cvssMetricV2.Type != "Primary" { - continue - } + + if len(cve.Metrics.CVSSMetricV2) > 0 { + slices.SortFunc(cve.Metrics.CVSSMetricV2, func(a nvdapi.CVSSMetricV2, b nvdapi.CVSSMetricV2) int { + if a.Type == "Primary" && b.Type != "Primary" { + return -1 + } else if a.Type != "Primary" && b.Type == "Primary" { + return 1 + } + + return 0 + }) + cvssMetricV2 := cve.Metrics.CVSSMetricV2[0] baseMetricV2 = &schema.NVDCVEFeedJSON10DefImpactBaseMetricV2{ AcInsufInfo: *cvssMetricV2.ACInsufInfo, CVSSV2: &schema.CVSSV20{ @@ -948,9 +957,21 @@ func convertAPI20CVEToLegacy(cve nvdapi.CVE, logger log.Logger) *schema.NVDCVEFe } var baseMetricV3 *schema.NVDCVEFeedJSON10DefImpactBaseMetricV3 - for _, cvssMetricV30 := range cve.Metrics.CVSSMetricV30 { - if cvssMetricV30.Type != "Primary" { - continue + var hasPrimaryCVSSv3 bool + if len(cve.Metrics.CVSSMetricV30) > 0 { + slices.SortFunc(cve.Metrics.CVSSMetricV30, func(a nvdapi.CVSSMetricV30, b nvdapi.CVSSMetricV30) int { + if a.Type == "Primary" && b.Type != "Primary" { + return -1 + } else if a.Type != "Primary" && b.Type == "Primary" { + return 1 + } + + return 0 + }) + + cvssMetricV30 := cve.Metrics.CVSSMetricV30[0] + if cvssMetricV30.Type == "Primary" { + hasPrimaryCVSSv3 = true } baseMetricV3 = &schema.NVDCVEFeedJSON10DefImpactBaseMetricV3{ CVSSV3: &schema.CVSSV30{ @@ -989,46 +1010,56 @@ func convertAPI20CVEToLegacy(cve nvdapi.CVE, logger log.Logger) *schema.NVDCVEFe ImpactScore: derefPtr((*float64)(cvssMetricV30.ImpactScore)), } } - // Use CVSSMetricV31 if available (override CVSSMetricV30) - for _, cvssMetricV31 := range cve.Metrics.CVSSMetricV31 { - if cvssMetricV31.Type != "Primary" { - continue - } - baseMetricV3 = &schema.NVDCVEFeedJSON10DefImpactBaseMetricV3{ - CVSSV3: &schema.CVSSV30{ - AttackComplexity: derefPtr(cvssMetricV31.CVSSData.AttackComplexity), - AttackVector: derefPtr(cvssMetricV31.CVSSData.AttackVector), - AvailabilityImpact: derefPtr(cvssMetricV31.CVSSData.AvailabilityImpact), - AvailabilityRequirement: derefPtr(cvssMetricV31.CVSSData.AvailabilityRequirement), - BaseScore: cvssMetricV31.CVSSData.BaseScore, - BaseSeverity: cvssMetricV31.CVSSData.BaseSeverity, - ConfidentialityImpact: derefPtr(cvssMetricV31.CVSSData.ConfidentialityImpact), - ConfidentialityRequirement: derefPtr(cvssMetricV31.CVSSData.ConfidentialityRequirement), - EnvironmentalScore: derefPtr(cvssMetricV31.CVSSData.EnvironmentalScore), - EnvironmentalSeverity: derefPtr(cvssMetricV31.CVSSData.EnvironmentalSeverity), - ExploitCodeMaturity: derefPtr(cvssMetricV31.CVSSData.ExploitCodeMaturity), - IntegrityImpact: derefPtr(cvssMetricV31.CVSSData.IntegrityImpact), - IntegrityRequirement: derefPtr(cvssMetricV31.CVSSData.IntegrityRequirement), - ModifiedAttackComplexity: derefPtr(cvssMetricV31.CVSSData.ModifiedAttackComplexity), - ModifiedAttackVector: derefPtr(cvssMetricV31.CVSSData.ModifiedAttackVector), - ModifiedAvailabilityImpact: derefPtr(cvssMetricV31.CVSSData.ModifiedAvailabilityImpact), - ModifiedConfidentialityImpact: derefPtr(cvssMetricV31.CVSSData.ModifiedConfidentialityImpact), - ModifiedIntegrityImpact: derefPtr(cvssMetricV31.CVSSData.ModifiedIntegrityImpact), - ModifiedPrivilegesRequired: derefPtr(cvssMetricV31.CVSSData.ModifiedPrivilegesRequired), - ModifiedScope: derefPtr(cvssMetricV31.CVSSData.ModifiedScope), - ModifiedUserInteraction: derefPtr(cvssMetricV31.CVSSData.ModifiedUserInteraction), - PrivilegesRequired: derefPtr(cvssMetricV31.CVSSData.PrivilegesRequired), - RemediationLevel: derefPtr(cvssMetricV31.CVSSData.RemediationLevel), - ReportConfidence: derefPtr(cvssMetricV31.CVSSData.ReportConfidence), - Scope: derefPtr(cvssMetricV31.CVSSData.Scope), - TemporalScore: derefPtr(cvssMetricV31.CVSSData.TemporalScore), - TemporalSeverity: derefPtr(cvssMetricV31.CVSSData.TemporalSeverity), - UserInteraction: derefPtr(cvssMetricV31.CVSSData.UserInteraction), - VectorString: cvssMetricV31.CVSSData.VectorString, - Version: cvssMetricV31.CVSSData.Version, - }, - ExploitabilityScore: derefPtr((*float64)(cvssMetricV31.ExploitabilityScore)), - ImpactScore: derefPtr((*float64)(cvssMetricV31.ImpactScore)), + // Use CVSSMetricV31 if available (override CVSSMetricV30 unless 3.0 is primary and 3.1 is not) + if len(cve.Metrics.CVSSMetricV31) > 0 { + slices.SortFunc(cve.Metrics.CVSSMetricV31, func(a nvdapi.CVSSMetricV31, b nvdapi.CVSSMetricV31) int { + if a.Type == "Primary" && b.Type != "Primary" { + return -1 + } else if a.Type != "Primary" && b.Type == "Primary" { + return 1 + } + + return 0 + }) + + cvssMetricV31 := cve.Metrics.CVSSMetricV31[0] + if cvssMetricV31.Type == "Primary" || !hasPrimaryCVSSv3 { + baseMetricV3 = &schema.NVDCVEFeedJSON10DefImpactBaseMetricV3{ + CVSSV3: &schema.CVSSV30{ + AttackComplexity: derefPtr(cvssMetricV31.CVSSData.AttackComplexity), + AttackVector: derefPtr(cvssMetricV31.CVSSData.AttackVector), + AvailabilityImpact: derefPtr(cvssMetricV31.CVSSData.AvailabilityImpact), + AvailabilityRequirement: derefPtr(cvssMetricV31.CVSSData.AvailabilityRequirement), + BaseScore: cvssMetricV31.CVSSData.BaseScore, + BaseSeverity: cvssMetricV31.CVSSData.BaseSeverity, + ConfidentialityImpact: derefPtr(cvssMetricV31.CVSSData.ConfidentialityImpact), + ConfidentialityRequirement: derefPtr(cvssMetricV31.CVSSData.ConfidentialityRequirement), + EnvironmentalScore: derefPtr(cvssMetricV31.CVSSData.EnvironmentalScore), + EnvironmentalSeverity: derefPtr(cvssMetricV31.CVSSData.EnvironmentalSeverity), + ExploitCodeMaturity: derefPtr(cvssMetricV31.CVSSData.ExploitCodeMaturity), + IntegrityImpact: derefPtr(cvssMetricV31.CVSSData.IntegrityImpact), + IntegrityRequirement: derefPtr(cvssMetricV31.CVSSData.IntegrityRequirement), + ModifiedAttackComplexity: derefPtr(cvssMetricV31.CVSSData.ModifiedAttackComplexity), + ModifiedAttackVector: derefPtr(cvssMetricV31.CVSSData.ModifiedAttackVector), + ModifiedAvailabilityImpact: derefPtr(cvssMetricV31.CVSSData.ModifiedAvailabilityImpact), + ModifiedConfidentialityImpact: derefPtr(cvssMetricV31.CVSSData.ModifiedConfidentialityImpact), + ModifiedIntegrityImpact: derefPtr(cvssMetricV31.CVSSData.ModifiedIntegrityImpact), + ModifiedPrivilegesRequired: derefPtr(cvssMetricV31.CVSSData.ModifiedPrivilegesRequired), + ModifiedScope: derefPtr(cvssMetricV31.CVSSData.ModifiedScope), + ModifiedUserInteraction: derefPtr(cvssMetricV31.CVSSData.ModifiedUserInteraction), + PrivilegesRequired: derefPtr(cvssMetricV31.CVSSData.PrivilegesRequired), + RemediationLevel: derefPtr(cvssMetricV31.CVSSData.RemediationLevel), + ReportConfidence: derefPtr(cvssMetricV31.CVSSData.ReportConfidence), + Scope: derefPtr(cvssMetricV31.CVSSData.Scope), + TemporalScore: derefPtr(cvssMetricV31.CVSSData.TemporalScore), + TemporalSeverity: derefPtr(cvssMetricV31.CVSSData.TemporalSeverity), + UserInteraction: derefPtr(cvssMetricV31.CVSSData.UserInteraction), + VectorString: cvssMetricV31.CVSSData.VectorString, + Version: cvssMetricV31.CVSSData.Version, + }, + ExploitabilityScore: derefPtr((*float64)(cvssMetricV31.ExploitabilityScore)), + ImpactScore: derefPtr((*float64)(cvssMetricV31.ImpactScore)), + } } } diff --git a/server/vulnerabilities/nvd/sync/cve_syncer_test.go b/server/vulnerabilities/nvd/sync/cve_syncer_test.go index acbc0656f0..b44d9024e7 100644 --- a/server/vulnerabilities/nvd/sync/cve_syncer_test.go +++ b/server/vulnerabilities/nvd/sync/cve_syncer_test.go @@ -95,6 +95,14 @@ func compareVulnerabilities(v1 schema.NVDCVEFeedJSON10DefCVEItem, v2 schema.NVDC v.CVE.Problemtype = nil } + // We now fall back to secondary CVSS score sources when primary isn't available, which legacy didn't do + if v1.Impact.BaseMetricV2 == nil { + v2.Impact.BaseMetricV2 = nil + } + if v1.Impact.BaseMetricV3 == nil { + v2.Impact.BaseMetricV3 = nil + } + clearDifferingFields(&v1) clearDifferingFields(&v2) return cmp.Equal(v1, v2) diff --git a/server/vulnerabilities/nvd/sync/testdata/cve/vulncheck_test_data/nvdcve-1.1-2024-expected.json b/server/vulnerabilities/nvd/sync/testdata/cve/vulncheck_test_data/nvdcve-1.1-2024-expected.json index afc2ea80f5..bab5c5e935 100644 --- a/server/vulnerabilities/nvd/sync/testdata/cve/vulncheck_test_data/nvdcve-1.1-2024-expected.json +++ b/server/vulnerabilities/nvd/sync/testdata/cve/vulncheck_test_data/nvdcve-1.1-2024-expected.json @@ -146,7 +146,26 @@ } ] }, - "impact": {}, + "impact": { + "baseMetricV3": { + "cvssV3": { + "attackComplexity": "LOW", + "attackVector": "LOCAL", + "availabilityImpact": "HIGH", + "baseScore": 7.8, + "baseSeverity": "HIGH", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "privilegesRequired": "NONE", + "scope": "UNCHANGED", + "userInteraction": "REQUIRED", + "vectorString": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.0" + }, + "exploitabilityScore": 1.8, + "impactScore": 5.9 + } + }, "lastModifiedDate": "2024-04-03T17:24Z", "publishedDate": "2024-04-03T17:15Z" }, @@ -183,7 +202,26 @@ "configurations": { "CVE_data_version": "4.0" }, - "impact": {}, + "impact": { + "baseMetricV3": { + "cvssV3": { + "attackComplexity": "LOW", + "attackVector": "LOCAL", + "availabilityImpact": "HIGH", + "baseScore": 7.8, + "baseSeverity": "HIGH", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "privilegesRequired": "NONE", + "scope": "UNCHANGED", + "userInteraction": "REQUIRED", + "vectorString": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.0" + }, + "exploitabilityScore": 1.8, + "impactScore": 5.9 + } + }, "lastModifiedDate": "2024-04-03T17:24Z", "publishedDate": "2024-04-03T17:15Z" }, @@ -235,7 +273,26 @@ } ] }, - "impact": {}, + "impact": { + "baseMetricV3": { + "cvssV3": { + "attackComplexity": "LOW", + "attackVector": "LOCAL", + "availabilityImpact": "HIGH", + "baseScore": 7.8, + "baseSeverity": "HIGH", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "privilegesRequired": "NONE", + "scope": "UNCHANGED", + "userInteraction": "REQUIRED", + "vectorString": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.0" + }, + "exploitabilityScore": 1.8, + "impactScore": 5.9 + } + }, "lastModifiedDate": "2024-04-03T17:24Z", "publishedDate": "2024-04-03T17:15Z" },