mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 00:18:27 +00:00
Feature: Improve our capability to detect vulnerable software on Ubuntu hosts To improve the capability of detecting vulnerable software on Ubuntu, we are now using OVAL definitions to detect vulnerable software on Ubuntu hosts. If data sync is enabled (disable_data_sync=false) OVAL definitions are automatically kept up to date (they are 'refreshed' once per day) - there's also the option to manually download the OVAL definitions using the 'fleetctl vulnerability-data-stream' command. Downloaded definitions are then parsed into an intermediary format and then used to identify vulnerable software on Ubuntu hosts. Finally, any 'recent' detected vulnerabilities are sent to any third-party integrations.
281 lines
6.7 KiB
Go
281 lines
6.7 KiB
Go
package vulnerabilities
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/facebookincubator/nvdtools/cvefeed"
|
|
feednvd "github.com/facebookincubator/nvdtools/cvefeed/nvd"
|
|
"github.com/facebookincubator/nvdtools/providers/nvd"
|
|
"github.com/facebookincubator/nvdtools/wfn"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
|
|
kitlog "github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
)
|
|
|
|
// DownloadNVDCVEFeed downloads the NVD CVE feed. Skips downloading if the cve feed has not changed since the last time.
|
|
func DownloadNVDCVEFeed(vulnPath string, cveFeedPrefixURL string) error {
|
|
cve := nvd.SupportedCVE["cve-1.1.json.gz"]
|
|
|
|
source := nvd.NewSourceConfig()
|
|
if cveFeedPrefixURL != "" {
|
|
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
|
|
}
|
|
|
|
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.*\.gz$`)
|
|
|
|
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
|
|
}
|
|
|
|
type softwareCPEWithNVDMeta struct {
|
|
fleet.SoftwareCPE
|
|
meta *wfn.Attributes
|
|
}
|
|
|
|
// 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,
|
|
) ([]fleet.SoftwareVulnerability, error) {
|
|
files, err := getNVDCVEFeedFiles(vulnPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(files) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Skip entries from platforms supported by OVAL
|
|
CPEs, err := ds.ListSoftwareCPEs(ctx, oval.SupportedHostPlatforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var parsed []softwareCPEWithNVDMeta
|
|
for _, CPE := range CPEs {
|
|
// Skip dummy CPEs
|
|
if strings.HasPrefix(CPE.CPE, "none") {
|
|
continue
|
|
}
|
|
|
|
attr, err := wfn.Parse(CPE.CPE)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsed = append(parsed, softwareCPEWithNVDMeta{
|
|
SoftwareCPE: CPE,
|
|
meta: attr,
|
|
})
|
|
}
|
|
if len(parsed) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var vulns []fleet.SoftwareVulnerability
|
|
for _, file := range files {
|
|
r, err := checkCVEs(ctx, ds, logger, parsed, file, collectVulns)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vulns = append(vulns, r...)
|
|
}
|
|
|
|
return vulns, nil
|
|
}
|
|
|
|
func checkCVEs(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
logger kitlog.Logger,
|
|
softwareCPEs []softwareCPEWithNVDMeta,
|
|
file string,
|
|
collectVulns bool,
|
|
) ([]fleet.SoftwareVulnerability, error) {
|
|
dict, err := cvefeed.LoadJSONDictionary(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cache := cvefeed.NewCache(dict).SetRequireVersion(true).SetMaxSize(-1)
|
|
// This index consumes too much RAM
|
|
// cache.Idx = cvefeed.NewIndex(dict)
|
|
|
|
softwareCPECh := make(chan softwareCPEWithNVDMeta)
|
|
|
|
var results []fleet.SoftwareVulnerability
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
|
|
for i := 0; i < runtime.NumCPU(); i++ {
|
|
wg.Add(1)
|
|
goRoutineKey := i
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
logKey := fmt.Sprintf("cpe-processing-%d", goRoutineKey)
|
|
level.Debug(logger).Log(logKey, "start")
|
|
|
|
for {
|
|
select {
|
|
case softwareCPE, more := <-softwareCPECh:
|
|
if !more {
|
|
level.Debug(logger).Log(logKey, "done")
|
|
return
|
|
}
|
|
|
|
cacheHits := cache.Get([]*wfn.Attributes{softwareCPE.meta})
|
|
for _, matches := range cacheHits {
|
|
ml := len(matches.CPEs)
|
|
if ml == 0 {
|
|
continue
|
|
}
|
|
|
|
matchingVulns := make([]fleet.SoftwareVulnerability, 0, ml)
|
|
cveID := matches.CVE.ID()
|
|
for _, attr := range matches.CPEs {
|
|
if attr == nil {
|
|
level.Error(logger).Log("matches nil CPE", cveID)
|
|
continue
|
|
}
|
|
matchingVulns = append(matchingVulns, fleet.SoftwareVulnerability{
|
|
SoftwareID: softwareCPE.SoftwareID,
|
|
CPEID: softwareCPE.ID,
|
|
CVE: cveID,
|
|
})
|
|
}
|
|
|
|
newCount, err := ds.InsertVulnerabilities(ctx, matchingVulns, fleet.NVD)
|
|
if err != nil {
|
|
level.Error(logger).Log("cpe processing", "error", "err", err)
|
|
continue // do not report a recent vuln that failed to be inserted in the DB
|
|
}
|
|
|
|
// collect vuln only if newCount > 0, otherwise we would send
|
|
// webhook requests for the same vulnerability over and over again until
|
|
// it is older than 2 days.
|
|
if collectVulns && newCount > 0 {
|
|
_, ok := matches.CVE.(*feednvd.Vuln)
|
|
if !ok {
|
|
level.Error(logger).Log(
|
|
"recent vuln", "unexpected type for Vuln interface",
|
|
"cve", cveID,
|
|
"type", fmt.Sprintf("%T", matches.CVE))
|
|
continue
|
|
}
|
|
|
|
mu.Lock()
|
|
results = append(results, matchingVulns...)
|
|
mu.Unlock()
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
level.Debug(logger).Log(logKey, "quitting")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
level.Debug(logger).Log("pushing cpes", "start")
|
|
for _, cpe := range softwareCPEs {
|
|
softwareCPECh <- cpe
|
|
}
|
|
close(softwareCPECh)
|
|
|
|
level.Debug(logger).Log("pushing cpes", "done")
|
|
|
|
wg.Wait()
|
|
return results, nil
|
|
}
|
|
|
|
// TODO (juan): Remove this after OVAL centos
|
|
// PostProcess performs additional processing over the results of
|
|
// the main vulnerability processing run (TranslateSoftwareToCPE+TranslateCPEToCVE).
|
|
func PostProcess(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
vulnPath string,
|
|
logger kitlog.Logger,
|
|
config config.FleetConfig,
|
|
) error {
|
|
dbPath := filepath.Join(vulnPath, "cpe.sqlite")
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open cpe database: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := centosPostProcessing(ctx, ds, db, logger, config); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|