mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Improved cpe deterministic matching (#42325)
**Related issue:** Resolves #41644 There are two cases that exist in the cpe database where this generic logic could not be applied. django from python_packages: gofiber:django djangoproject:django npm from npm_packages: microsoft:npm npmjs:npm These will require individual cve overrides that is outside the scope of this task. - [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/guides/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), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Enhanced CPE (Common Platform Enumeration) matching to reduce non-deterministic vendor selection when multiple vendors exist for the same software product. The algorithm now incorporates software ecosystem information to ensure more accurate and consistent vulnerability resolution across package types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
2e6ffa747d
commit
a599889152
5 changed files with 157 additions and 5 deletions
1
changes/41644-improve-cpe-matching
Normal file
1
changes/41644-improve-cpe-matching
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added software source to ecosystem matching to help prevent non-deterministic CPE selection when multiple vendors exist for the same product.
|
||||
|
|
@ -120,8 +120,10 @@ type cpeSearchQuery struct {
|
|||
args []any
|
||||
}
|
||||
|
||||
const cpeSelectColumns = `SELECT c.rowid, c.product, c.vendor, c.deprecated FROM cpe_2 c`
|
||||
const cpeOrderBy = ` ORDER BY c.vendor, c.product`
|
||||
const (
|
||||
cpeSelectColumns = `SELECT c.rowid, c.product, c.vendor, c.target_sw, c.deprecated FROM cpe_2 c`
|
||||
cpeOrderBy = ` ORDER BY c.vendor, c.product`
|
||||
)
|
||||
|
||||
// cpeSearchQueries returns individual search queries in priority order for finding CPE matches.
|
||||
// Query 1 (vendor+product) and 2 (product-only) are cheap index lookups. Query 3 (full-text search)
|
||||
|
|
@ -192,6 +194,72 @@ func cpeVendorMatchesSoftware(item *IndexedCPEItem, software *fleet.Software) bo
|
|||
return matched
|
||||
}
|
||||
|
||||
// cpeTargetSWMatchesSoftware returns a score (0-3) indicating how well the CPE's vendor
|
||||
// and target_sw fields match the expected ecosystem for the software's source.
|
||||
func cpeTargetSWMatchesSoftware(item *IndexedCPEItem, software *fleet.Software) int {
|
||||
expectedTargetSW := targetSW(software)
|
||||
|
||||
if expectedTargetSW != "*" {
|
||||
// Best match: CPE's target_sw matches what we expect for this software source
|
||||
// Example:
|
||||
// software.source="npm_packages" (expectedTargetSW="node.js")
|
||||
// item.TargetSW="node.js"
|
||||
if item.TargetSW != "" && strings.EqualFold(item.TargetSW, expectedTargetSW) {
|
||||
return 3
|
||||
}
|
||||
|
||||
// Good match: CPE vendor contains the ecosystem name
|
||||
// Example:
|
||||
// software.source="python_packages" (expectedTargetSW="python")
|
||||
// item.Vendor="python"
|
||||
expectedLower := strings.ToLower(expectedTargetSW)
|
||||
vendorLower := strings.ToLower(item.Vendor)
|
||||
|
||||
// "node.js" -> "node"
|
||||
ecosystemName := expectedLower
|
||||
if strings.Contains(ecosystemName, ".") {
|
||||
ecosystemName = strings.Split(ecosystemName, ".")[0]
|
||||
}
|
||||
|
||||
if strings.Contains(vendorLower, ecosystemName) {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
if expectedTargetSW == "*" {
|
||||
// Good match: CPE vendor contains the ecosystem name
|
||||
vendorLower := strings.ToLower(item.Vendor)
|
||||
switch software.Source {
|
||||
case "deb_packages":
|
||||
// Example:
|
||||
// software.source="deb_packages" (expectedTargetSW="*")
|
||||
// item.Vendor="debian"
|
||||
if strings.Contains(vendorLower, "debian") {
|
||||
return 2
|
||||
}
|
||||
case "rpm_packages":
|
||||
// Example:
|
||||
// software.source="rpm_packages" (expectedTargetSW="*")
|
||||
// item.Vendor="redhat"
|
||||
if strings.Contains(vendorLower, "redhat") || strings.Contains(vendorLower, "fedora") {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Partial match: CPE vendor matches software name with common _project suffix
|
||||
// Example:
|
||||
// software.name="duplicity", source="python_packages"
|
||||
// item.Vendor="duplicity_project", item.Product="duplicity"
|
||||
productLower := strings.ToLower(item.Product)
|
||||
vendorLower := strings.ToLower(item.Vendor)
|
||||
if vendorLower == productLower+"_project" {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// cpeItemMatchesSoftware checks whether a CPE result's vendor/product terms all appear in the
|
||||
// software's name, vendor, and bundle identifier.
|
||||
func cpeItemMatchesSoftware(item *IndexedCPEItem, software *fleet.Software) bool {
|
||||
|
|
@ -644,6 +712,8 @@ func CPEFromSoftware(ctx context.Context, logger *slog.Logger, db *sqlx.DB, soft
|
|||
// This avoids nondeterministic results when multiple CPE entries match
|
||||
// (e.g. "ge:line" vs "linecorp:line" for the "Line" app).
|
||||
var bestMatch *IndexedCPEItem
|
||||
var bestTargetSWScore int
|
||||
var bestVendorMatch bool
|
||||
var deprecatedMatches []IndexedCPEItem
|
||||
for i := range results {
|
||||
if !cpeItemMatchesSoftware(&results[i], software) {
|
||||
|
|
@ -653,8 +723,19 @@ func CPEFromSoftware(ctx context.Context, logger *slog.Logger, db *sqlx.DB, soft
|
|||
deprecatedMatches = append(deprecatedMatches, results[i])
|
||||
continue
|
||||
}
|
||||
if bestMatch == nil || (!cpeVendorMatchesSoftware(bestMatch, software) && cpeVendorMatchesSoftware(&results[i], software)) {
|
||||
|
||||
targetSWScore := cpeTargetSWMatchesSoftware(&results[i], software)
|
||||
vendorMatch := cpeVendorMatchesSoftware(&results[i], software)
|
||||
|
||||
// first valid match, OR
|
||||
// better target_sw score (ecosystem match), OR
|
||||
// Same target_sw score but better vendor match
|
||||
if bestMatch == nil ||
|
||||
targetSWScore > bestTargetSWScore ||
|
||||
(targetSWScore == bestTargetSWScore && !bestVendorMatch && vendorMatch) {
|
||||
bestMatch = &results[i]
|
||||
bestTargetSWScore = targetSWScore
|
||||
bestVendorMatch = vendorMatch
|
||||
}
|
||||
}
|
||||
if bestMatch != nil {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,42 @@ func TestCPEFromSoftware(t *testing.T) {
|
|||
)
|
||||
require.NoError(t, err, "software name %q should not cause FTS5 syntax error", name)
|
||||
}
|
||||
|
||||
// Target_SW scoring: python_packages source should prefer python vendor over jenkins vendor
|
||||
// when multiple CPE entries exist for the same product name.
|
||||
cpe, err = CPEFromSoftware(t.Context(), slog.New(slog.DiscardHandler), db, &fleet.Software{
|
||||
Name: "requests", Version: "2.31.0", Source: "python_packages",
|
||||
}, nil, reCache)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cpe:2.3:a:python:requests:2.31.0:*:*:*:*:python:*:*", cpe,
|
||||
"python_packages should prefer python:requests (vendor contains 'python')")
|
||||
|
||||
// Target_SW scoring: npm_packages source should prefer openjsf vendor over checkpoint vendor
|
||||
// when the CPE has target_sw=node.js matching the expected target_sw.
|
||||
cpe, err = CPEFromSoftware(t.Context(), slog.New(slog.DiscardHandler), db, &fleet.Software{
|
||||
Name: "express", Version: "4.18.0", Source: "npm_packages",
|
||||
}, nil, reCache)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cpe:2.3:a:openjsf:express:4.18.0:*:*:*:*:node.js:*:*", cpe,
|
||||
"npm_packages should prefer openjsf:express with target_sw=node.js")
|
||||
|
||||
// Target_SW scoring with <product>_project fallback pattern.
|
||||
// For duplicity from python_packages, neither vendor relates to Python ecosystem, but
|
||||
// duplicity_project:duplicity follows the NVD "<product>_project" pattern for upstream CPEs.
|
||||
cpe, err = CPEFromSoftware(t.Context(), slog.New(slog.DiscardHandler), db, &fleet.Software{
|
||||
Name: "duplicity", Version: "0.8.0", Source: "python_packages",
|
||||
}, nil, reCache)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cpe:2.3:a:duplicity_project:duplicity:0.8.0:*:*:*:*:python:*:*", cpe,
|
||||
"should prefer duplicity_project (upstream) over debian (distro-specific) using _project pattern")
|
||||
|
||||
// Target_SW scoring: deb_packages source should prefer debian vendor for duplicity
|
||||
cpe, err = CPEFromSoftware(t.Context(), slog.New(slog.DiscardHandler), db, &fleet.Software{
|
||||
Name: "duplicity", Version: "0.8.0", Source: "deb_packages",
|
||||
}, nil, reCache)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cpe:2.3:a:debian:duplicity:0.8.0:*:*:*:*:*:*:*", cpe,
|
||||
"deb_packages duplicity should prefer debian:duplicity (vendor contains 'debian')")
|
||||
}
|
||||
|
||||
func TestCPETranslations(t *testing.T) {
|
||||
|
|
@ -1112,7 +1148,16 @@ func TestCPEFromSoftwareIntegration(t *testing.T) {
|
|||
Version: "0.8.21",
|
||||
Vendor: "",
|
||||
BundleIdentifier: "",
|
||||
}, cpe: "cpe:2.3:a:debian:duplicity:0.8.21:*:*:*:*:python:*:*",
|
||||
}, cpe: "cpe:2.3:a:duplicity_project:duplicity:0.8.21:*:*:*:*:python:*:*",
|
||||
},
|
||||
{
|
||||
software: fleet.Software{
|
||||
Name: "duplicity",
|
||||
Source: "deb_packages",
|
||||
Version: "0.8.21",
|
||||
Vendor: "",
|
||||
BundleIdentifier: "",
|
||||
}, cpe: "cpe:2.3:a:debian:duplicity:0.8.21:*:*:*:*:*:*:*",
|
||||
},
|
||||
{
|
||||
software: fleet.Software{
|
||||
|
|
@ -1346,7 +1391,7 @@ func TestCPEFromSoftwareIntegration(t *testing.T) {
|
|||
Version: "2.25.1",
|
||||
Vendor: "",
|
||||
BundleIdentifier: "",
|
||||
}, cpe: "cpe:2.3:a:jenkins:requests:2.25.1:*:*:*:*:python:*:*",
|
||||
}, cpe: "cpe:2.3:a:python:requests:2.25.1:*:*:*:*:python:*:*",
|
||||
},
|
||||
{
|
||||
software: fleet.Software{
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type IndexedCPEItem struct {
|
|||
Product string `json:"product" db:"product"`
|
||||
Vendor string `json:"vendor" db:"vendor"`
|
||||
SWEdition string `json:"sw_edition" db:"sw_edition"`
|
||||
TargetSW string `json:"target_sw" db:"target_sw"`
|
||||
Deprecated bool `json:"deprecated" db:"deprecated"`
|
||||
Weight int `db:"weight"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,5 +68,29 @@ const XmlCPETestDict = `
|
|||
<title xml:lang="en-US">Good Corp Correct Result 1.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:goodcorp:correct_result:1.0:*:*:*:*:*:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:python:requests:2.31.0">
|
||||
<title xml:lang="en-US">Python Requests 2.31.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:python:requests:2.31.0:*:*:*:*:*:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:jenkins:requests:2.31.0">
|
||||
<title xml:lang="en-US">Jenkins Requests 2.31.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:jenkins:requests:2.31.0:*:*:*:*:jenkins:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:duplicity_project:duplicity:0.8.0">
|
||||
<title xml:lang="en-US">Duplicity Project Duplicity 0.8.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:duplicity_project:duplicity:0.8.0:*:*:*:*:*:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:debian:duplicity:0.8.0">
|
||||
<title xml:lang="en-US">Debian Duplicity 0.8.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:debian:duplicity:0.8.0:*:*:*:*:*:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:openjsf:express:4.18.0">
|
||||
<title xml:lang="en-US">OpenJS Foundation Express 4.18.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:openjsf:express:4.18.0:*:*:*:*:node.js:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:checkpoint:express:4.18.0">
|
||||
<title xml:lang="en-US">Check Point Express 4.18.0</title>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:checkpoint:express:4.18.0:*:*:*:*:*:*:*"/>
|
||||
</cpe-item>
|
||||
</cpe-list>
|
||||
`
|
||||
|
|
|
|||
Loading…
Reference in a new issue