fleet/server/vulnerabilities/goval_dictionary/analyzer.go
Ian Littman e96c70e4c0
Pull xz'd goval-dictionary sqlite files to evaluate vulnerabilities on Amazon Linux hosts (#21506)
#20934

This is tied to https://github.com/fleetdm/vulnerabilities/pull/14; for
supported OS versions (currently Amazon Linux 1/2/2022/2023) we'll pull
XZ'd sqlite files from the vulnerabilities repo and query them to
determine what's vulnerable. See the associated issue for how I
self-QA'd this.

This replaced OVAL parsing for Amazon Linux 2, as we were using the
wrong data source there (Amazon has backported a bunch of fixes to their
own-named releases, so any RHEL fixes don't match).

Some checklist items are missing here; getting this set up in draft to
get code feedback now, and I'll push updates with e.g. docs changes, as
well ass an addition to the changes file.

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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/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)
- [x] Added/updated tests
    - [x] Add tests to oval_platform
    - [x] Add sync_test
    - [x] Add database_test
- [x] Manual QA for all new/changed functionality
- [x] Update vulnerability management docs
2024-08-26 14:07:42 -05:00

139 lines
3.5 KiB
Go

package goval_dictionary
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
kitlog "github.com/go-kit/log"
)
const (
hostsBatchSize = 500
vulnBatchSize = 500
)
var ErrUnsupportedPlatform = errors.New("unsupported platform")
// Analyze scans all hosts for vulnerabilities based on the sqlite output of goval-dictionary
// for their platform, inserting any new vulnerabilities and deleting anything patched.
// Returns nil, nil when the platform isn't supported.
func Analyze(
ctx context.Context,
ds fleet.Datastore,
ver fleet.OSVersion,
vulnPath string,
collectVulns bool,
logger kitlog.Logger,
) ([]fleet.SoftwareVulnerability, error) {
platform := oval.NewPlatform(ver.Platform, ver.Name)
source := fleet.GovalDictionarySource
if !platform.IsGovalDictionarySupported() {
return nil, ErrUnsupportedPlatform
}
db, err := loadDb(platform, vulnPath)
if err != nil {
return nil, err
}
// Since hosts and software have a M:N relationship, the following sets are used to
// avoid doing duplicated inserts/delete operations (a vulnerable software might be
// present in many hosts).
toInsertSet := make(map[string]fleet.SoftwareVulnerability)
toDeleteSet := make(map[string]fleet.SoftwareVulnerability)
var offset int
for {
hostIDs, err := ds.HostIDsByOSVersion(ctx, ver, offset, hostsBatchSize)
if err != nil {
return nil, err
}
if len(hostIDs) == 0 {
break
}
offset += hostsBatchSize
foundInBatch := make(map[uint][]fleet.SoftwareVulnerability)
for _, hostID := range hostIDs {
hostID := hostID
software, err := ds.ListSoftwareForVulnDetection(ctx, fleet.VulnSoftwareFilter{HostID: &hostID})
if err != nil {
return nil, err
}
vulnerabilities := db.Eval(software, logger)
foundInBatch[hostID] = vulnerabilities
}
existingInBatch, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, hostIDs, source)
if err != nil {
return nil, err
}
for _, hostID := range hostIDs {
inserts, deletes := utils.VulnsDelta(foundInBatch[hostID], existingInBatch[hostID])
for _, i := range inserts {
toInsertSet[i.Key()] = i
}
for _, d := range deletes {
toDeleteSet[d.Key()] = d
}
}
}
err = utils.BatchProcess(toDeleteSet, func(v []fleet.SoftwareVulnerability) error {
return ds.DeleteSoftwareVulnerabilities(ctx, v)
}, vulnBatchSize)
if err != nil {
return nil, err
}
var inserted []fleet.SoftwareVulnerability
if collectVulns {
inserted = make([]fleet.SoftwareVulnerability, 0, len(toInsertSet))
}
err = utils.BatchProcess(toInsertSet, func(vulns []fleet.SoftwareVulnerability) error {
for _, v := range vulns {
ok, err := ds.InsertSoftwareVulnerability(ctx, v, source)
if err != nil {
return err
}
if collectVulns && ok {
inserted = append(inserted, v)
}
}
return nil
}, vulnBatchSize)
if err != nil {
return nil, err
}
return inserted, nil
}
// loadDb returns the latest goval_dictionary database for the given platform.
func loadDb(platform oval.Platform, vulnPath string) (*Database, error) {
if !platform.IsGovalDictionarySupported() {
return nil, fmt.Errorf("platform %q not supported", platform)
}
fileName := platform.ToGovalDictionaryFilename()
latest, err := utils.LatestFile(fileName, vulnPath)
if err != nil {
return nil, err
}
sqlite, err := sql.Open("sqlite3", latest)
if err != nil {
return nil, err
}
db := NewDB(sqlite, platform)
return db, nil
}