Include non-primary CVSS scores from NVD when a primary score doesn'texist for a given CVSS version (#29199)

Fixes #28261.

~~Of note, this logic will prefer a non-primary CVSSv3.1 score over a
primary CVSSv3.0 score if 3.1 doesn't have primary but 3.0 does. I
haven't seen any evidence of this in our dataset (looked at 2024
output).~~

Updated with logic that will prefer a primary CVSSv3.0 score over a
secondary CVSSv3.1 score for a given vulnerability. In the test dataset
(2023 vuln snapshot, ~20k vulns) there were no cases where this
situation presented itself, so output was identical to the prior
implementation.

Validated by comparing a vulns run from GitHub Actions to a local run
with the new code, and confirmed that existing v3 scores weren't
replaced when they already existed (just got adds of v2 when only v3
existed, and v2/v3 adds when no scoring existed).

Confirmed that all three CVEs mentioned in #28261 show up in feed data.
Added spot-checks for secondary CVSS scores to the feed validator tool.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Ian Littman
2025-05-29 13:03:19 -05:00
committed by GitHub
parent f3ed4dcbe8
commit 7a54a2de22
6 changed files with 168 additions and 52 deletions
+4 -1
View File
@@ -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
+1
View File
@@ -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.
+17 -1
View File
@@ -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) {
+78 -47
View File
@@ -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)),
}
}
}
@@ -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)
@@ -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"
},