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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Vendored
+60
-3
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user