fleet/server/vulnerabilities/nvd/cve.go
jacobshandling 235a79eeaa
Generate correct CPE from malformed ipswitch whatsup CPE, ensure matches relevant CVEs (#41704)
**Related issue:** Resolves #32662 

# Checklist for submitter

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

- [ ] Changes file added for user-visible changes in `changes/`
- [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

* **New Features**
* Use CPE alias handling to generate correct CPE from malformed one,
ensuring correct CVEs are matched.

* **Tests**
* Added comprehensive test coverage for the enhanced CPE alias
expansion, including malformed CPE mapping scenarios and CVE matching
validation for Ipswitch WhatsUp.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-16 16:17:47 -05:00

798 lines
23 KiB
Go

package nvd
import (
"context"
"errors"
"fmt"
"io/fs"
"log/slog"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
nvdsync "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/sync"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed"
feednvd "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd/schema"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/providers/nvd"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
"github.com/google/go-github/v37/github"
)
const (
vulnRepo = "vulnerabilities"
)
// DownloadNVDCVEFeed downloads CVEs information from the NVD 2.0 API
// and supplements the data with CPE information from the Vulncheck API.
// This is used to download CVE information to vulnPath.
func GenerateCVEFeeds(vulnPath string, debug bool, logger *slog.Logger) error {
cveSyncer, err := nvdsync.NewCVE(
vulnPath,
nvdsync.WithLogger(logger),
nvdsync.WithDebug(debug),
)
if err != nil {
return err
}
if err := cveSyncer.Do(context.Background()); err != nil {
return fmt.Errorf("download nvd cve feed: %w", err)
}
if err := cveSyncer.DoVulnCheck(context.Background()); err != nil {
return fmt.Errorf("download nvd cve feed: %w", err)
}
return nil
}
func DownloadCVEFeed(vulnPath, cveFeedPrefixURL string, debug bool, logger *slog.Logger) error {
var err error
if cveFeedPrefixURL == "" {
cveFeedPrefixURL, err = GetGitHubCVEAssetPath()
if err != nil {
return fmt.Errorf("get cve asset path: %w", err)
}
}
err = downloadNVDCVELegacy(vulnPath, cveFeedPrefixURL)
if err != nil {
return fmt.Errorf("download nvd cve feed: %w", err)
}
return nil
}
func GetGitHubCVEAssetPath() (string, error) {
vulnOwner := os.Getenv("TEST_VULN_GITHUB_OWNER")
if vulnOwner == "" {
vulnOwner = owner
}
ghClient := github.NewClient(fleethttp.NewGithubClient())
releases, _, err := ghClient.Repositories.ListReleases(
context.Background(),
vulnOwner,
vulnRepo,
&github.ListOptions{Page: 0, PerPage: 10},
)
if err != nil {
return "", err
}
nvdregex := regexp.MustCompile(`cve-\d+`)
var found string
for _, release := range releases {
// Skip draft releases
if release.GetDraft() {
continue
}
if nvdregex.MatchString(release.GetTagName()) {
found = release.GetTagName()
break
}
}
if found == "" {
return "", errors.New("no CVE feed found")
}
return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/", vulnOwner, vulnRepo, found), nil
}
func downloadNVDCVELegacy(vulnPath string, cveFeedPrefixURL string) error {
if cveFeedPrefixURL == "" {
return errors.New("missing cve_feed_prefix_url")
}
source := nvd.NewSourceConfig()
parsed, err := url.Parse(cveFeedPrefixURL)
if err != nil {
return fmt.Errorf("parsing cve feed url prefix override: %w", err)
}
source.Host = parsed.Host
source.CVEFeedPath = parsed.Path
source.Scheme = parsed.Scheme
cve := nvd.SupportedCVE["cve-1.1.json.gz"]
dfs := nvd.Sync{
Feeds: []nvd.Syncer{cve},
Source: source,
LocalDir: vulnPath,
}
syncTimeout := 5 * time.Minute
if os.Getenv("NETWORK_TEST") != "" {
syncTimeout = 10 * time.Minute
}
ctx, cancelFunc := context.WithTimeout(context.Background(), syncTimeout)
defer cancelFunc()
if err := dfs.Do(ctx); err != nil {
return fmt.Errorf("download nvd cve feed: %w", err)
}
return nil
}
const publishedDateFmt = "2006-01-02T15:04Z" // not quite RFC3339
var rxNVDCVEArchive = regexp.MustCompile(`nvdcve.*\.json.*$`)
func getNVDCVEFeedFiles(vulnPath string) ([]string, error) {
var files []string
err := filepath.WalkDir(vulnPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if match := rxNVDCVEArchive.MatchString(path); !match {
return nil
}
files = append(files, path)
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
// interface for items with NVD Meta Data
type itemWithNVDMeta interface {
GetMeta() *wfn.Attributes
GetID() uint
}
type softwareCPEWithNVDMeta struct {
fleet.SoftwareCPE
meta *wfn.Attributes
}
func (s softwareCPEWithNVDMeta) GetMeta() *wfn.Attributes {
return s.meta
}
func (s softwareCPEWithNVDMeta) GetID() uint {
return s.SoftwareID
}
type osCPEWithNVDMeta struct {
fleet.OperatingSystem
meta *wfn.Attributes
}
func (o osCPEWithNVDMeta) GetMeta() *wfn.Attributes {
return o.meta
}
func (o osCPEWithNVDMeta) GetID() uint {
return o.ID
}
// TranslateCPEToCVE maps the CVEs found in NVD archive files in the
// vulnerabilities database folder to software CPEs in the fleet database.
// If collectVulns is true, returns a list of any new software vulnerabilities found.
func TranslateCPEToCVE(
ctx context.Context,
ds fleet.Datastore,
vulnPath string,
logger *slog.Logger,
collectVulns bool,
startTime time.Time,
) ([]fleet.SoftwareVulnerability, error) {
files, err := getNVDCVEFeedFiles(vulnPath)
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, nil
}
// get all the software CPEs from the database
CPEs, err := ds.ListSoftwareCPEs(ctx)
if err != nil {
return nil, err
}
// hydrate the CPEs with the meta data
var parsed []softwareCPEWithNVDMeta
for _, CPE := range CPEs {
attr, err := wfn.Parse(CPE.CPE)
if err != nil {
return nil, err
}
parsed = append(parsed, softwareCPEWithNVDMeta{
SoftwareCPE: CPE,
meta: attr,
})
}
cpes, err := GetMacOSCPEs(ctx, ds)
if err != nil {
return nil, err
}
if len(parsed) == 0 && len(cpes) == 0 {
return nil, nil
}
var interfaceParsed []itemWithNVDMeta
for _, p := range parsed {
interfaceParsed = append(interfaceParsed, p)
}
for _, c := range cpes {
interfaceParsed = append(interfaceParsed, c)
}
knownNVDBugRules, err := GetKnownNVDBugRules()
if err != nil {
return nil, err
}
// we are using a map here to remove any duplicates - a vulnerability can be present in more than one
// NVD feed file.
softwareVulns := make(map[string]fleet.SoftwareVulnerability)
osVulns := make(map[string]fleet.OSVulnerability)
for _, file := range files {
foundSoftwareVulns, foundOSVulns, err := checkCVEs(
ctx,
logger,
interfaceParsed,
file,
knownNVDBugRules,
)
if err != nil {
return nil, err
}
for _, e := range foundSoftwareVulns {
softwareVulns[e.Key()] = e
}
for _, e := range foundOSVulns {
osVulns[e.Key()] = e
}
}
// Batch insert software vulnerabilities.
allSoftwareVulns := make([]fleet.SoftwareVulnerability, 0, len(softwareVulns))
for _, vuln := range softwareVulns {
allSoftwareVulns = append(allSoftwareVulns, vuln)
}
newVulns, softwareInsertErr := ds.InsertSoftwareVulnerabilities(ctx, allSoftwareVulns, fleet.NVDSource)
if softwareInsertErr != nil {
logger.ErrorContext(ctx, "cpe processing error", "err", softwareInsertErr)
}
if !collectVulns {
newVulns = nil
}
// Batch insert OS vulnerabilities.
allOSVulns := make([]fleet.OSVulnerability, 0, len(osVulns))
for _, vuln := range osVulns {
allOSVulns = append(allOSVulns, vuln)
}
osInsertErr := false
if _, err := ds.InsertOSVulnerabilities(ctx, allOSVulns, fleet.NVDSource); err != nil {
logger.ErrorContext(ctx, "cpe processing error", "err", err)
osInsertErr = true
}
// Delete any stale vulnerabilities. A vulnerability is stale iff the last time it was
// updated was more than `2 * periodicity` ago. This assumes that the whole vulnerability
// process completes in less than `periodicity` units of time.
//
// This is used to get rid of false positives once they are fixed and no longer detected as vulnerabilities.
// Skip cleanup when the corresponding insert failed to avoid deleting data with nothing to replace it.
if softwareInsertErr == nil {
if err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, startTime); err != nil {
logger.ErrorContext(ctx, "error deleting out of date vulnerabilities", "err", err)
}
}
if !osInsertErr {
if err = ds.DeleteOutOfDateOSVulnerabilities(ctx, fleet.NVDSource, startTime); err != nil {
logger.ErrorContext(ctx, "error deleting out of date OS vulnerabilities", "err", err)
}
}
return newVulns, nil
}
// GetMacOSCPEs translates all found macOS Operating Systems to CPEs.
func GetMacOSCPEs(ctx context.Context, ds fleet.Datastore) ([]osCPEWithNVDMeta, error) {
var cpes []osCPEWithNVDMeta
oses, err := ds.ListOperatingSystemsForPlatform(ctx, "darwin")
if err != nil {
return cpes, ctxerr.Wrap(ctx, err, "list operating systems")
}
if len(oses) == 0 {
return cpes, nil
}
// variants of macOS found in the NVD feed
macosVariants := []string{"macos", "mac_os_x"}
for _, os := range oses {
for _, variant := range macosVariants {
versionParts := strings.Split(os.Version, ".")
if len(versionParts) == 2 {
// Vulncheck reports versions with all 3 parts, so pad with an extra 0 if we only
// have 2 parts (15.3 -> 15.3.0)
versionParts = append(versionParts, "0")
os.Version = strings.Join(versionParts, ".")
}
cpe := osCPEWithNVDMeta{
OperatingSystem: os,
meta: &wfn.Attributes{
Part: "o",
Vendor: "apple",
Product: variant,
Version: os.Version,
Update: wfn.Any,
Edition: wfn.Any,
SWEdition: wfn.Any,
TargetSW: wfn.Any,
TargetHW: wfn.Any,
Other: wfn.Any,
Language: wfn.Any,
},
}
cpes = append(cpes, cpe)
}
}
return cpes, nil
}
func matchesExactTargetSW(softwareCPETargetSW string, targetSWs []string, configs []*wfn.Attributes) bool {
for _, targetSW := range targetSWs {
if softwareCPETargetSW == targetSW {
for _, attr := range configs {
if attr.TargetSW == targetSW {
return true
}
}
}
}
return false
}
func checkCVEs(
ctx context.Context,
logger *slog.Logger,
cpeItems []itemWithNVDMeta,
jsonFile string,
knownNVDBugRules CPEMatchingRules,
) ([]fleet.SoftwareVulnerability, []fleet.OSVulnerability, error) {
dict, err := cvefeed.LoadJSONDictionary(jsonFile)
if err != nil {
return nil, nil, err
}
// Group dictionary by vendor using a map.
// This is done to speed up the matching process (PR https://github.com/fleetdm/fleet/pull/17298).
// A map uses a hash table to store the key-value pairs. By putting multiple vulnerabilities with the same vendor into a map,
// we reduce the number of comparisons needed to find the vulnerabilities that match the CPEs. Specifically, we no longer need to
// compare each CPE with each vulnerability, but only with the vulnerabilities that have the same vendor.
// Further optimization can be done by also using a map for product name comparison.
dictGrouped := make(map[string]cvefeed.Dictionary, len(dict))
for key, vuln := range dict {
attrsArray := vuln.Config()
for _, attrs := range attrsArray {
subDict, ok := dictGrouped[attrs.Vendor]
if !ok {
subDict = make(cvefeed.Dictionary, 1)
dictGrouped[attrs.Vendor] = subDict
}
subDict[key] = vuln
}
}
cacheGrouped := make(map[string]*cvefeed.Cache, len(dictGrouped))
for vendor, subDict := range dictGrouped {
cache := cvefeed.NewCache(subDict).SetRequireVersion(true).SetMaxSize(-1)
cacheGrouped[vendor] = cache
}
CPEItemCh := make(chan itemWithNVDMeta)
var foundSoftwareVulns []fleet.SoftwareVulnerability
var foundOSVulns []fleet.OSVulnerability
var wg sync.WaitGroup
var softwareMu sync.Mutex
var osMu sync.Mutex
logger = logger.With("json_file", jsonFile)
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(1)
goRoutineKey := i
go func() {
defer wg.Done()
logger := logger.With("routine", goRoutineKey)
logger.DebugContext(ctx, "start")
for {
select {
case CPEItem, more := <-CPEItemCh:
if !more {
logger.DebugContext(ctx, "done")
return
}
cache, ok := cacheGrouped[CPEItem.GetMeta().Vendor]
if !ok {
// No such vendor in the Vulnerability dictionary
continue
}
cpeItemsWithAliases := expandCPEAliases(CPEItem.GetMeta())
for _, cpeItem := range cpeItemsWithAliases {
cacheHits := cache.Get([]*wfn.Attributes{cpeItem})
for _, matches := range cacheHits {
if len(matches.CPEs) == 0 {
continue
}
if rule, ok := knownNVDBugRules.FindMatch(
matches.CVE.ID(),
); ok {
if !rule.CPEMatches(cpeItem) {
continue
}
}
// For chrome/firefox extensions we only want to match vulnerabilities
// that are reported explicitly for target_sw == "chrome" or target_sw = "firefox".
//
// Why? In many occasions the NVD dataset reports vulnerabilities in client applications
// with target_sw == "*", meaning the client application is vulnerable on all operating systems.
// Such rules we want to ignore here to prevent many false positives that do not apply to the
// Chrome or Firefox environment.
if cpeItem.TargetSW == "chrome" || cpeItem.TargetSW == "firefox" {
if !matchesExactTargetSW(
cpeItem.TargetSW,
[]string{"chrome", "firefox"},
matches.CVE.Config(),
) {
continue
}
}
resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), cpeItem, dict, logger)
if err != nil {
logger.DebugContext(ctx, "version end excluding error", "err", err)
}
if _, ok := CPEItem.(softwareCPEWithNVDMeta); ok {
vuln := fleet.SoftwareVulnerability{
SoftwareID: CPEItem.GetID(),
CVE: matches.CVE.ID(),
ResolvedInVersion: ptr.String(resolvedVersion),
}
softwareMu.Lock()
foundSoftwareVulns = append(foundSoftwareVulns, vuln)
softwareMu.Unlock()
} else if _, ok := CPEItem.(osCPEWithNVDMeta); ok {
vuln := fleet.OSVulnerability{
OSID: CPEItem.GetID(),
CVE: matches.CVE.ID(),
ResolvedInVersion: ptr.String(resolvedVersion),
}
osMu.Lock()
foundOSVulns = append(foundOSVulns, vuln)
osMu.Unlock()
}
}
}
case <-ctx.Done():
logger.DebugContext(ctx, "quitting")
return
}
}
}()
}
logger.DebugContext(ctx, "pushing cpes")
for _, cpe := range cpeItems {
CPEItemCh <- cpe
}
close(CPEItemCh)
logger.DebugContext(ctx, "cpes pushed")
wg.Wait()
return foundSoftwareVulns, foundOSVulns, nil
}
var pythonVersionWithUpdate = regexp.MustCompile(`(alpha|beta|rc)(\d+)`)
// expandCPEAliases will generate new *wfn.Attributes from the given cpeItem.
// It returns a slice with the given cpeItem plus the generated *wfn.Attributes.
//
// We need this because entries in the CPE database are not consistent.
// E.g. some Visual Studio Code extensions are defined with target_sw=visual_studio_code
// and others are defined with target_sw=visual_studio.
// E.g. The python extension for Visual Studio Code is defined with
// product=python_extension,target_sw=visual_studio_code and with
// product=visual_studio_code,target_sw=python.
func expandCPEAliases(cpeItem *wfn.Attributes) []*wfn.Attributes {
cpeItems := []*wfn.Attributes{cpeItem}
// Some VSCode extensions are defined with target_sw=visual_studio_code
// and others are defined with target_sw=visual_studio.
for _, cpeItem := range cpeItems {
if cpeItem.TargetSW == "visual_studio_code" {
cpeItem2 := *cpeItem
cpeItem2.TargetSW = "visual_studio"
cpeItems = append(cpeItems, &cpeItem2)
}
}
// cpe:2.3:a:microsoft:python_extension:2024.2.1:*:*:*:*:visual_studio_code:*:*
// cpe:2.3:a:microsoft:visual_studio_code:2024.2.1:*:*:*:*:python:*:*
// cpe:2.3:a:microsoft:python:2020.4.0:*:*:*:*:visual_studio_code:*:*
for _, cpeItem := range cpeItems {
if cpeItem.TargetSW == "visual_studio_code" &&
cpeItem.Vendor == "microsoft" &&
cpeItem.Product == "python_extension" {
cpeItem2 := *cpeItem
cpeItem2.Product = "visual_studio_code"
cpeItem2.TargetSW = "python"
cpeItems = append(cpeItems, &cpeItem2)
cpeItem3 := *cpeItem
cpeItem3.Product = "python"
cpeItems = append(cpeItems, &cpeItem3)
}
}
for _, cpeItem := range cpeItems {
if cpeItem.Vendor == "oracle" && cpeItem.Product == "virtualbox" {
cpeItem2 := *cpeItem
cpeItem2.Product = "vm_virtualbox"
cpeItems = append(cpeItems, &cpeItem2)
}
}
// The NVD CPE dictionary contains an invalid CPE for Ipswitch WhatsUp with product="whatsup",
// but CVE-2006-2354 references product="whatsup_professional".
// See https://github.com/fleetdm/fleet/issues/32662.
for _, cpeItem := range cpeItems {
if cpeItem.Vendor == "ipswitch" && cpeItem.Product == "whatsup" {
cpeItem2 := *cpeItem
cpeItem2.Product = "whatsup_professional"
cpeItems = append(cpeItems, &cpeItem2)
}
}
// pgAdmin CVEs in NVD use target_sw=postgresql and product=pgadmin_4, but Fleet generates
// CPEs with platform-based target_sw (macos, windows) and may use different product
// names (pgadmin, pgadmin4). Add aliases with target_sw=postgresql and product name
// variations to match NVD's criteria.
// See https://github.com/fleetdm/fleet/issues/37957.
for _, cpeItem := range cpeItems {
if cpeItem.Vendor == "pgadmin" &&
(cpeItem.Product == "pgadmin_4" || cpeItem.Product == "pgadmin" || cpeItem.Product == "pgadmin4") {
// Add aliases with product name variations and target_sw=postgresql
for _, productName := range []string{"pgadmin", "pgadmin_4", "pgadmin4"} {
newItem := *cpeItem
newItem.Product = productName
newItem.TargetSW = "postgresql"
cpeItems = append(cpeItems, &newItem)
}
}
}
// Python pre-release versions can have the pre-release part in the version field or in the
// update field (the technically correct place). We generate the "correct" CPEs (with the
// pre-release part in the update field), so we have to create an alias here with the
// pre-release part in the version field to cover all the cases.
// e.g. Python 3.14.0 alpha2 can be represented as both:
// 1. cpe:2.3:a:python:python:3.14.0:alpha2:*:*:*:windows:*:*
// 2. cpe:2.3:a:python:python:3.14.0a2:*:*:*:*:windows:*:*
// We generate CPEs like 1, but in the feed (e.g. Vulncheck) it can also appear as 2.
// See https://github.com/fleetdm/fleet/issues/25882.
for _, cpeItem := range cpeItems {
if cpeItem.Vendor == "python" &&
cpeItem.Product == "python" &&
cpeItem.Update != "" &&
pythonVersionWithUpdate.MatchString(cpeItem.Update) {
cpeItem2 := *cpeItem
for _, submatches := range pythonVersionWithUpdate.FindAllStringSubmatchIndex(cpeItem2.Update, -1) {
prefixBytes := []byte{}
numberBytes := []byte{}
prefixBytes = pythonVersionWithUpdate.ExpandString(prefixBytes, "${1}", cpeItem.Update, submatches)
numberBytes = pythonVersionWithUpdate.ExpandString(numberBytes, "${2}", cpeItem.Update, submatches)
var prefix string
switch prefixBytes[0] {
case 'a':
prefix = string(prefixBytes[0])
case 'b':
prefix = string(prefixBytes[0])
case 'r':
prefix = string(prefixBytes)
}
cpeItem2.Version = fmt.Sprintf("%s%s%s", cpeItem.Version, prefix, string(numberBytes))
cpeItem2.Update = ""
}
cpeItems = append(cpeItems, &cpeItem2)
}
}
return cpeItems
}
// Returns the versionEndExcluding string for the given CVE and host software meta
// data, if it exists in the NVD feed. This effectively gives us the version of the
// software it needs to upgrade to in order to address the CVE.
func getMatchingVersionEndExcluding(ctx context.Context, cve string, hostSoftwareMeta *wfn.Attributes, dict cvefeed.Dictionary, logger *slog.Logger) (string, error) {
vuln, ok := dict[cve].(*feednvd.Vuln)
if !ok {
return "", nil
}
// Schema() maps to the JSON schema of the NVD feed for a given CVE
vulnSchema := vuln.Schema()
if vulnSchema == nil {
logger.ErrorContext(ctx, "error getting schema for CVE", "cve", cve)
return "", nil
}
config := vulnSchema.Configurations
if config == nil {
return "", nil
}
nodes := config.Nodes
if len(nodes) == 0 {
return "", nil
}
cpeMatch := findCPEMatch(nodes)
if len(cpeMatch) == 0 {
return "", nil
}
// Check if the host software version matches any of the CPEMatch rules.
// CPEMatch rules can include version strings for the following:
// - versionStartIncluding
// - versionStartExcluding
// - versionEndExcluding
// - versionEndIncluding - not used in this function as we don't want to assume the resolved version
// Back slashes are added to the version string during parsing; remove them to ensure that the version
// comparison works correctly. See https://github.com/fleetdm/fleet/issues/25991.
hostSoftwareVersion := wfn.StripSlashes(hostSoftwareMeta.Version)
for _, rule := range cpeMatch {
if rule.VersionEndExcluding == "" {
continue
}
// convert the NVD cpe23URi to wfn.Attributes for later comparison
attr, err := wfn.Parse(rule.Cpe23Uri)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "parsing cpe23Uri")
}
// ensure the product and vendor match
if attr.Product != hostSoftwareMeta.Product || attr.Vendor != hostSoftwareMeta.Vendor {
continue
}
if attr.SWEdition != wfn.Any && attr.SWEdition != hostSoftwareMeta.SWEdition &&
!(hostSoftwareMeta.SWEdition == wfn.Any && attr.SWEdition == wfn.NA) {
continue
}
// versionEnd is the version string that the vulnerable host software version must be less than
versionEnd, err := checkVersion(rule, hostSoftwareVersion)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "checking version")
}
if versionEnd != "" {
return versionEnd, nil
}
}
return "", nil
}
// CPEMatch can be nested in Children nodes. Recursively search the nodes for a CPEMatch
func findCPEMatch(nodes []*schema.NVDCVEFeedJSON10DefNode) []*schema.NVDCVEFeedJSON10DefCPEMatch {
for _, node := range nodes {
if len(node.CPEMatch) > 0 {
return node.CPEMatch
}
if len(node.Children) > 0 {
match := findCPEMatch(node.Children)
if match != nil {
return match
}
}
}
return nil
}
// checkVersion checks if the host software version matches the CPEMatch rule
func checkVersion(rule *schema.NVDCVEFeedJSON10DefCPEMatch, softwareVersionStr string) (string, error) {
if rule.VersionStartIncluding == "" && rule.VersionStartExcluding == "" && rule.VersionEndExcluding == "" {
return rule.VersionEndExcluding, nil
}
if rule.VersionStartIncluding == "" && rule.VersionStartExcluding == "" {
// "softwareVersionStr < endExcluding",
if feednvd.SmartVerCmp(softwareVersionStr, rule.VersionEndExcluding) == -1 {
return rule.VersionEndExcluding, nil
}
}
if rule.VersionStartIncluding != "" {
// "softwareVersionStr >= startIncluding && softwareVersionStr < endExcluding"
if (feednvd.SmartVerCmp(softwareVersionStr, rule.VersionStartIncluding) == 1 || feednvd.SmartVerCmp(softwareVersionStr, rule.VersionStartIncluding) == 0) &&
feednvd.SmartVerCmp(softwareVersionStr, rule.VersionEndExcluding) == -1 {
return rule.VersionEndExcluding, nil
}
}
// "softwareVersionStr > startExcluding && softwareVersionStr < endExcluding"
if feednvd.SmartVerCmp(softwareVersionStr, rule.VersionStartExcluding) == 1 && feednvd.SmartVerCmp(softwareVersionStr, rule.VersionEndExcluding) == -1 {
return rule.VersionEndExcluding, nil
}
return "", nil
}