mirror of
https://github.com/fleetdm/fleet
synced 2026-05-09 18:20:48 +00:00
215 lines
5.4 KiB
Go
215 lines
5.4 KiB
Go
package msrc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/vulnerabilities/io"
|
|
msrc "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/parsed"
|
|
utils "github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
|
|
kitlog "github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
)
|
|
|
|
const (
|
|
vulnBatchSize = 500
|
|
)
|
|
|
|
func Analyze(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
os fleet.OperatingSystem,
|
|
vulnPath string,
|
|
collectVulns bool,
|
|
logger kitlog.Logger,
|
|
) ([]fleet.OSVulnerability, error) {
|
|
bulletin, err := loadBulletin(os, vulnPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Find matching products inside the bulletin
|
|
matchingPIDs := make(map[string]bool)
|
|
pID, err := bulletin.Products.GetMatchForOS(ctx, os)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "Analyzing MSRC vulnerabilities")
|
|
}
|
|
if pID != "" {
|
|
matchingPIDs[pID] = true
|
|
}
|
|
|
|
if len(matchingPIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
toInsert := make(map[string]fleet.OSVulnerability)
|
|
toDelete := make(map[string]fleet.OSVulnerability)
|
|
|
|
// Run vulnerability detection for all hosts in this batch (hIDs)
|
|
// and store the results in 'found'.
|
|
var found []fleet.OSVulnerability
|
|
for cve, v := range bulletin.Vulnerabities {
|
|
// Check if this vulnerability targets the OS
|
|
if !utils.ProductIDsIntersect(v.ProductIDs, matchingPIDs) {
|
|
continue
|
|
}
|
|
// Check if the OS is vulnerable to the vulnerability by referencing the OS kernel version
|
|
isVuln, riv := isOSVulnerable(os.KernelVersion, bulletin, v, matchingPIDs, cve, logger)
|
|
if isVuln {
|
|
found = append(found, fleet.OSVulnerability{
|
|
OSID: os.ID,
|
|
CVE: cve,
|
|
Source: fleet.MSRCSource,
|
|
ResolvedInVersion: ptr.String(riv),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Fetch all stored vulnerabilities for the current batch
|
|
osVulns, err := ds.ListOSVulnerabilitiesByOS(ctx, os.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var existing []fleet.OSVulnerability
|
|
existing = append(existing, osVulns...)
|
|
|
|
// Compute what needs to be inserted/deleted for this batch
|
|
insrt, del := utils.VulnsDelta(found, existing)
|
|
for _, i := range insrt {
|
|
toInsert[i.Key()] = i
|
|
}
|
|
for _, d := range del {
|
|
toDelete[d.Key()] = d
|
|
}
|
|
|
|
err = utils.BatchProcess(toDelete, func(v []fleet.OSVulnerability) error {
|
|
return ds.DeleteOSVulnerabilities(ctx, v)
|
|
}, vulnBatchSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var inserted []fleet.OSVulnerability
|
|
if collectVulns {
|
|
inserted = make([]fleet.OSVulnerability, 0, len(toInsert))
|
|
}
|
|
|
|
err = utils.BatchProcess(toInsert, func(v []fleet.OSVulnerability) error {
|
|
n, err := ds.InsertOSVulnerabilities(ctx, v, fleet.MSRCSource)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if collectVulns && n > 0 {
|
|
inserted = append(inserted, v...)
|
|
}
|
|
|
|
return nil
|
|
}, vulnBatchSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return inserted, nil
|
|
}
|
|
|
|
// isOSVulnerable returns true if the OS is vulnerable to the given vulnerability.
|
|
// If the OS is vulnerable, the function returns the version in which the vulnerability
|
|
// was resolved.
|
|
func isOSVulnerable(
|
|
osKernel string,
|
|
b *msrc.SecurityBulletin,
|
|
v msrc.Vulnerability,
|
|
matchingPIDs map[string]bool,
|
|
cve string,
|
|
logger kitlog.Logger,
|
|
) (isVulnerable bool, resolvedInVersion string) {
|
|
for KBID := range v.RemediatedBy {
|
|
fix := b.VendorFixes[KBID]
|
|
|
|
// Check if the MSRC FixedBuild version targets the OS version
|
|
if !utils.ProductIDsIntersect(fix.ProductIDs, matchingPIDs) {
|
|
continue
|
|
}
|
|
|
|
for _, build := range fix.FixedBuilds {
|
|
// An empty FixBuild is a bug in the MSRC feed, last
|
|
// seen around apr-2021. Ignoring it to avoid false
|
|
// positive vulnerabilities.
|
|
if build == "" {
|
|
continue
|
|
}
|
|
|
|
fixedBuild, feedParts, err := getBuildNumber(build)
|
|
if err != nil {
|
|
level.Debug(logger).Log("msg", "invalid msrc feed version", "cve", cve, "err", err)
|
|
continue
|
|
}
|
|
|
|
osBuild, osParts, err := getBuildNumber(osKernel)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// skip if the product version number does not match
|
|
// ie. 10.0.22000.X vs 10.0.22631.X
|
|
if isProductVersionMismatch(feedParts, osParts) {
|
|
continue
|
|
}
|
|
|
|
if osBuild < fixedBuild {
|
|
return true, build
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func isProductVersionMismatch(feedVersion, osVersion []string) bool {
|
|
for i := 0; i < 3; i++ {
|
|
if feedVersion[i] != osVersion[i] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// loadBulletin loads the most recent bulletin for the given os
|
|
func loadBulletin(os fleet.OperatingSystem, dir string) (*msrc.SecurityBulletin, error) {
|
|
product := msrc.NewProductFromOS(os)
|
|
fileName := io.MSRCFileName(product.Name(), time.Now())
|
|
|
|
latest, err := utils.LatestFile(fileName, dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return msrc.UnmarshalBulletin(latest)
|
|
}
|
|
|
|
// getBuildNumber expects a version string in the format "10.0.22000.194" and
|
|
// returns the final part (build number) as an integer and the parts as a slice of strings.
|
|
func getBuildNumber(version string) (int, []string, error) {
|
|
if version == "" {
|
|
return 0, nil, fmt.Errorf("empty version string %s", version)
|
|
}
|
|
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) != 4 {
|
|
return 0, nil, fmt.Errorf("parts count mismatch %s", version)
|
|
}
|
|
|
|
build, err := strconv.Atoi(parts[3])
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("unable to parse build number %s", version)
|
|
}
|
|
|
|
return build, parts, nil
|
|
}
|