mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
**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 -->
798 lines
23 KiB
Go
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
|
|
}
|