fleet/server/vulnerabilities/msrc/analyzer.go
Victor Lyuboslavsky e2d9a9016c
Add gosimple linter (#23250)
#23249

Add gosimple linter to golangci-lint CI job.
2024-10-29 14:17:51 -05:00

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
}