mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 00:18:27 +00:00
Fixes #26404. This means that for long vulns runs vulns will stick around longer, so we don't wind up nuking vulns that were added earlier in the run, and in cases where the vulns run takes less than 2h we'll see vulns clear cleanly more quickly. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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) ## Testing - [x] Added/updated automated tests - [ ] QA'd all new/changed functionality manually --------- Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
757 lines
21 KiB
Go
757 lines
21 KiB
Go
package nvd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"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/go-kit/log"
|
|
kitlog "github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"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 log.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 log.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 kitlog.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
|
|
}
|
|
}
|
|
|
|
var newVulns []fleet.SoftwareVulnerability
|
|
for _, vuln := range softwareVulns {
|
|
ok, err := ds.InsertSoftwareVulnerability(ctx, vuln, fleet.NVDSource)
|
|
if err != nil {
|
|
level.Error(logger).Log("cpe processing", "error", "err", err)
|
|
continue
|
|
}
|
|
|
|
// collect vuln only if inserted, otherwise we would send
|
|
// webhook requests for the same vulnerability over and over again until
|
|
// it is older than 2 days.
|
|
if collectVulns && ok {
|
|
newVulns = append(newVulns, vuln)
|
|
}
|
|
}
|
|
|
|
for _, vuln := range osVulns {
|
|
_, err := ds.InsertOSVulnerability(ctx, vuln, fleet.NVDSource)
|
|
if err != nil {
|
|
level.Error(logger).Log("cpe processing", "error", "err", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
if err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, startTime); err != nil {
|
|
level.Error(logger).Log("msg", "error deleting out of date vulnerabilities", "err", err)
|
|
}
|
|
if err = ds.DeleteOutOfDateOSVulnerabilities(ctx, fleet.NVDSource, startTime); err != nil {
|
|
level.Error(logger).Log("msg", "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 kitlog.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 = log.With(logger, "json_file", jsonFile)
|
|
|
|
for i := 0; i < runtime.NumCPU(); i++ {
|
|
wg.Add(1)
|
|
goRoutineKey := i
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
logger := log.With(logger, "routine", goRoutineKey)
|
|
level.Debug(logger).Log("msg", "start")
|
|
|
|
for {
|
|
select {
|
|
case CPEItem, more := <-CPEItemCh:
|
|
if !more {
|
|
level.Debug(logger).Log("msg", "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 {
|
|
level.Debug(logger).Log("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():
|
|
level.Debug(logger).Log("msg", "quitting")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
level.Debug(logger).Log("msg", "pushing cpes")
|
|
|
|
for _, cpe := range cpeItems {
|
|
CPEItemCh <- cpe
|
|
}
|
|
close(CPEItemCh)
|
|
level.Debug(logger).Log("msg", "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)
|
|
}
|
|
}
|
|
|
|
// The python extension is defined in two ways in the CPE database:
|
|
// 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:*:*
|
|
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)
|
|
}
|
|
}
|
|
|
|
for _, cpeItem := range cpeItems {
|
|
if cpeItem.Vendor == "oracle" && cpeItem.Product == "virtualbox" {
|
|
cpeItem2 := *cpeItem
|
|
cpeItem2.Product = "vm_virtualbox"
|
|
cpeItems = append(cpeItems, &cpeItem2)
|
|
}
|
|
}
|
|
|
|
// 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 kitlog.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 {
|
|
level.Error(logger).Log("msg", "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
|
|
}
|
|
|
|
// 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
|
|
}
|