fleet/server/vulnerabilities/oval/analyzer.go
Jonathan Katz fd45d302f5
Add false-positive filtering for OVAL scanning (#33357)
**Related issue:** Resolves #31968 

# 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.

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [ ] Confirmed that the fix is not expected to adversely impact load
test results
- [ ] Alerted the release DRI if additional load testing is needed
2025-09-25 16:28:27 -04:00

193 lines
4.6 KiB
Go

package oval
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
oval_parsed "github.com/fleetdm/fleet/v4/server/vulnerabilities/oval/parsed"
utils "github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
)
const (
hostsBatchSize = 500
vulnBatchSize = 500
)
var ErrUnsupportedPlatform = errors.New("unsupported platform")
// Analyze scans all hosts for vulnerabilities based on the OVAL definitions 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,
) ([]fleet.SoftwareVulnerability, error) {
platform := NewPlatform(ver.Platform, ver.Name)
source := fleet.UbuntuOVALSource
if platform.IsRedHat() {
source = fleet.RHELOVALSource
}
if !platform.IsSupported() {
return nil, ErrUnsupportedPlatform
}
defs, err := loadDef(platform, vulnPath)
if err != nil {
return nil, err
}
rules, err := GetKnownOVALBugRules()
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
}
evalR, err := defs.Eval(ver, software)
if err != nil {
return nil, err
}
foundInBatch[hostID] = evalR
evalU, err := defs.EvalKernel(software)
if err != nil {
return nil, err
}
foundInBatch[hostID] = append(foundInBatch[hostID], evalU...)
// Create a map of id: software for each
// pair (id, cve) in foundInBatch for this host
softwareIDs := make(map[uint]fleet.Software)
for _, s := range software {
softwareIDs[s.ID] = s
}
filteredBatch := make([]fleet.SoftwareVulnerability, 0, len(foundInBatch[hostID]))
for _, v := range foundInBatch[hostID] {
software := softwareIDs[v.SoftwareID]
skip := rules.MatchesAny(software, v.CVE)
if !skip {
filteredBatch = append(filteredBatch, v)
}
}
foundInBatch[hostID] = filteredBatch
}
existingInBatch, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, hostIDs, source)
if err != nil {
return nil, err
}
for _, hostID := range hostIDs {
insrt, del := utils.VulnsDelta(foundInBatch[hostID], existingInBatch[hostID])
for _, i := range insrt {
toInsertSet[i.Key()] = i
}
for _, d := range del {
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
}
// loadDef returns the latest oval Definition for the given platform.
func loadDef(platform Platform, vulnPath string) (oval_parsed.Result, error) {
if !platform.IsSupported() {
return nil, fmt.Errorf("platform %q not supported", platform)
}
fileName := platform.ToFilename(time.Now(), "json")
latest, err := utils.LatestFile(fileName, vulnPath)
if err != nil {
return nil, err
}
payload, err := os.ReadFile(latest)
if err != nil {
return nil, err
}
if platform.IsUbuntu() {
result := oval_parsed.UbuntuResult{}
if err := json.Unmarshal(payload, &result); err != nil {
return nil, err
}
return result, nil
}
if platform.IsRedHat() {
result := oval_parsed.RhelResult{}
if err := json.Unmarshal(payload, &result); err != nil {
return nil, err
}
return result, nil
}
return nil, fmt.Errorf("don't know how to parse file %q for %q platform", latest, platform)
}