Feature 7076: Ingest installed windows updates (#7138)

* Ingest installed Windows updates and store them in the windows_updates table. 
* Added config option for enabling/disabling Windows update ingestion and Windows OS vuln. detection.
This commit is contained in:
Juan Fernandez 2022-08-26 14:55:03 -04:00 committed by GitHub
parent 55af48910a
commit 3048a07fd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1616 additions and 58 deletions

View File

@ -0,0 +1,3 @@
- We now ingest and store installed Windows updates in a new table, `windows_updates`.
- Added a new configuration option used for disabling the ingestion of Windows updates and also
disabling Windows vulnerability scans.

View File

@ -506,7 +506,7 @@ spec:
enable_vulnerabilities_webhook: false
host_batch_size: 0
`
expectedJSON := `
expectedJson := `
{
"kind": "config",
"apiVersion": "v1",
@ -581,7 +581,7 @@ spec:
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "config"}))
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "config", "--yaml"}))
assert.JSONEq(t, expectedJSON, runAppForTest(t, []string{"get", "config", "--json"}))
assert.JSONEq(t, expectedJson, runAppForTest(t, []string{"get", "config", "--json"}))
})
t.Run("IncludeServerConfig", func(t *testing.T) {
@ -661,6 +661,7 @@ spec:
cve_feed_prefix_url: ""
databases_path: ""
disable_data_sync: false
disable_win_os_vulnerabilities: false
periodicity: 0s
recent_vulnerability_max_age: 0s
vulnerability_settings:
@ -687,7 +688,10 @@ spec:
"kind": "config",
"apiVersion": "v1",
"spec": {
"org_info": { "org_name": "", "org_logo_url": "" },
"org_info": {
"org_name": "",
"org_logo_url": ""
},
"server_settings": {
"server_url": "",
"live_query_disabled": false,
@ -728,8 +732,12 @@ spec:
"enable_sso": false,
"enable_sso_idp_login": false
},
"fleet_desktop": { "transparency_url": "https://fleetdm.com/transparency" },
"vulnerability_settings": { "databases_path": "/some/path" },
"fleet_desktop": {
"transparency_url": "https://fleetdm.com/transparency"
},
"vulnerability_settings": {
"databases_path": "/some/path"
},
"webhook_settings": {
"host_status_webhook": {
"enable_host_status_webhook": false,
@ -750,7 +758,10 @@ spec:
},
"interval": "0s"
},
"integrations": { "jira": null, "zendesk": null },
"integrations": {
"jira": null,
"zendesk": null
},
"update_interval": {
"osquery_detail": "1h0m0s",
"osquery_policy": "1h0m0s"
@ -762,9 +773,13 @@ spec:
"cve_feed_prefix_url": "",
"current_instance_checks": "",
"disable_data_sync": false,
"recent_vulnerability_max_age": "0s"
"recent_vulnerability_max_age": "0s",
"disable_win_os_vulnerabilities": false
},
"license": {
"tier": "free",
"expiration": "0001-01-01T00:00:00Z"
},
"license": { "tier": "free", "expiration": "0001-01-01T00:00:00Z" },
"logging": {
"debug": true,
"json": false,

View File

@ -437,16 +437,16 @@ func extract(src, dst string) {
}
}
func loadUbuntuSoftware(ver string) []fleet.Software {
func loadSoftware(platform string, ver string) []fleet.Software {
srcPath := filepath.Join(
"..",
"..",
"server",
"vulnerabilities",
"testdata",
"ubuntu",
platform,
"software",
fmt.Sprintf("ubuntu_%s-software.json.bz2", ver),
fmt.Sprintf("%s_%s-software.json.bz2", platform, ver),
)
tmpDir, err := ioutil.TempDir("", "osquery-perf")
@ -461,18 +461,20 @@ func loadUbuntuSoftware(ver string) []fleet.Software {
type softwareJSON struct {
Name string `json:"name"`
Version string `json:"version"`
Release string `json:"release,omitempty"`
Arch string `json:"arch,omitempty"`
}
var software []softwareJSON
contents, err := ioutil.ReadFile(dstPath)
if err != nil {
log.Printf("reading vuln software for ubuntu %s: %s\n", ver, err)
log.Printf("reading vuln software for %s %s: %s\n", platform, ver, err)
return nil
}
err = json.Unmarshal(contents, &software)
if err != nil {
log.Printf("unmarshalling vuln software for ubuntu %s:%s", ver, err)
log.Printf("unmarshalling vuln software for %s %s:%s", platform, ver, err)
return nil
}
@ -487,28 +489,32 @@ func loadUbuntuSoftware(ver string) []fleet.Software {
return r
}
func (a *agent) SoftwareWindows11() []fleet.Software {
return loadSoftware("windows", "11")
}
func (a *agent) SoftwareUbuntu1604() []fleet.Software {
return loadUbuntuSoftware("1604")
return loadSoftware("ubuntu", "1604")
}
func (a *agent) SoftwareUbuntu1804() []fleet.Software {
return loadUbuntuSoftware("1804")
return loadSoftware("ubuntu", "1804")
}
func (a *agent) SoftwareUbuntu2004() []fleet.Software {
return loadUbuntuSoftware("2004")
return loadSoftware("ubuntu", "2004")
}
func (a *agent) SoftwareUbuntu2104() []fleet.Software {
return loadUbuntuSoftware("2104")
return loadSoftware("ubuntu", "2104")
}
func (a *agent) SoftwareUbuntu2110() []fleet.Software {
return loadUbuntuSoftware("2110")
return loadSoftware("ubuntu", "2110")
}
func (a *agent) SoftwareUbuntu2204() []fleet.Software {
return loadUbuntuSoftware("2204")
return loadSoftware("ubuntu", "2204")
}
func (a *agent) SoftwareMacOS() []fleet.Software {
@ -776,6 +782,7 @@ func (a *agent) processQuery(name, query string) (handled bool, results []map[st
if err != nil {
panic(err)
}
return true, results, &statusOK
}
}
@ -851,6 +858,9 @@ func main() {
templateNames := []string{
"mac10.14.6.tmpl",
// Uncomment this to add windows hosts
// "windows_11.tmpl",
// Uncomment this to add ubuntu hosts with vulnerable software
// "partial_ubuntu.tmpl",
// "ubuntu_16.04.tmpl",

File diff suppressed because it is too large Load Diff

View File

@ -2118,6 +2118,21 @@ Maximum age of a vulnerability (a CVE) to be considered "recent". The age is cal
recent_vulnerability_max_age: 48h
```
### disable_win_os_vulnerabilities
If using osquery 5.4 or later, Fleet by default will fetch and store all applied Windows updates and use that for detecting Windows
vulnerabilities — which might be a writing-intensive process (depending on the number of Windows hosts
in your Fleet). Setting this to true will cause Fleet to skip both processes.
- Default value: false
- Environment variable: `FLEET_VULNERABILITIES_DISABLE_WIN_OS_VULNERABILITIES`
- Config file format:
```
vulnerabilities:
disable_win_os_vulnerabilities: true
```
##### Example YAML
```yaml

View File

@ -273,13 +273,14 @@ type LicenseConfig struct {
// VulnerabilitiesConfig defines configs related to vulnerability processing within Fleet.
type VulnerabilitiesConfig struct {
DatabasesPath string `json:"databases_path" yaml:"databases_path"`
Periodicity time.Duration `json:"periodicity" yaml:"periodicity"`
CPEDatabaseURL string `json:"cpe_database_url" yaml:"cpe_database_url"`
CVEFeedPrefixURL string `json:"cve_feed_prefix_url" yaml:"cve_feed_prefix_url"`
CurrentInstanceChecks string `json:"current_instance_checks" yaml:"current_instance_checks"`
DisableDataSync bool `json:"disable_data_sync" yaml:"disable_data_sync"`
RecentVulnerabilityMaxAge time.Duration `json:"recent_vulnerability_max_age" yaml:"recent_vulnerability_max_age"`
DatabasesPath string `json:"databases_path" yaml:"databases_path"`
Periodicity time.Duration `json:"periodicity" yaml:"periodicity"`
CPEDatabaseURL string `json:"cpe_database_url" yaml:"cpe_database_url"`
CVEFeedPrefixURL string `json:"cve_feed_prefix_url" yaml:"cve_feed_prefix_url"`
CurrentInstanceChecks string `json:"current_instance_checks" yaml:"current_instance_checks"`
DisableDataSync bool `json:"disable_data_sync" yaml:"disable_data_sync"`
RecentVulnerabilityMaxAge time.Duration `json:"recent_vulnerability_max_age" yaml:"recent_vulnerability_max_age"`
DisableWinOSVulnerabilities bool `json:"disable_win_os_vulnerabilities" yaml:"disable_win_os_vulnerabilities"`
}
// UpgradesConfig defines configs related to fleet server upgrades.
@ -649,6 +650,11 @@ func (man Manager) addConfigs() {
"Skips synchronizing data streams and expects them to be available in the databases_path.")
man.addConfigDuration("vulnerabilities.recent_vulnerability_max_age", 30*24*time.Hour,
"Maximum age of the published date of a vulnerability (CVE) to be considered 'recent'.")
man.addConfigBool(
"vulnerabilities.disable_win_os_vulnerabilities",
false,
"Don't sync installed Windows updates nor perform Windows OS vulnerability processing.",
)
// Upgrades
man.addConfigBool("upgrades.allow_missing_migrations", false,
@ -846,13 +852,14 @@ func (man Manager) LoadConfig() FleetConfig {
EnforceHostLimit: man.getConfigBool("license.enforce_host_limit"),
},
Vulnerabilities: VulnerabilitiesConfig{
DatabasesPath: man.getConfigString("vulnerabilities.databases_path"),
Periodicity: man.getConfigDuration("vulnerabilities.periodicity"),
CPEDatabaseURL: man.getConfigString("vulnerabilities.cpe_database_url"),
CVEFeedPrefixURL: man.getConfigString("vulnerabilities.cve_feed_prefix_url"),
CurrentInstanceChecks: man.getConfigString("vulnerabilities.current_instance_checks"),
DisableDataSync: man.getConfigBool("vulnerabilities.disable_data_sync"),
RecentVulnerabilityMaxAge: man.getConfigDuration("vulnerabilities.recent_vulnerability_max_age"),
DatabasesPath: man.getConfigString("vulnerabilities.databases_path"),
Periodicity: man.getConfigDuration("vulnerabilities.periodicity"),
CPEDatabaseURL: man.getConfigString("vulnerabilities.cpe_database_url"),
CVEFeedPrefixURL: man.getConfigString("vulnerabilities.cve_feed_prefix_url"),
CurrentInstanceChecks: man.getConfigString("vulnerabilities.current_instance_checks"),
DisableDataSync: man.getConfigBool("vulnerabilities.disable_data_sync"),
RecentVulnerabilityMaxAge: man.getConfigDuration("vulnerabilities.recent_vulnerability_max_age"),
DisableWinOSVulnerabilities: man.getConfigBool("vulnerabilities.disable_win_os_vulnerabilities"),
},
Upgrades: UpgradesConfig{
AllowMissingMigrations: man.getConfigBool("upgrades.allow_missing_migrations"),

View File

@ -290,6 +290,7 @@ var hostRefs = []string{
"host_device_auth",
"host_batteries",
"host_operating_system",
"windows_updates",
}
func (ds *Datastore) DeleteHost(ctx context.Context, hid uint) error {

View File

@ -4558,7 +4558,10 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
// Update host_operating_system
err = ds.UpdateHostOperatingSystem(context.Background(), host.ID, fleet.OperatingSystem{Name: "foo", Version: "bar"})
require.NoError(t, err)
// Insert a windows update for the host
stmt := `INSERT INTO windows_updates (host_id, date_epoch, kb_id) VALUES (?, ?, ?)`
_, err = ds.writer.Exec(stmt, host.ID, 1, 123)
require.NoError(t, err)
// Check there's an entry for the host in all the associated tables.
for _, hostRef := range hostRefs {
var ok bool

View File

@ -0,0 +1,31 @@
package tables
import (
"database/sql"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20220809091020, Down_20220809091020)
}
func Up_20220809091020(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE windows_updates (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
host_id INT UNSIGNED NOT NULL,
date_epoch INT UNSIGNED NOT NULL,
kb_id INT UNSIGNED NOT NULL,
UNIQUE KEY idx_unique_windows_updates (host_id, kb_id),
KEY idx_update_date (host_id, date_epoch)
)`)
if err != nil {
return errors.Wrapf(err, "create operating_systems table")
}
return nil
}
func Down_20220809091020(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,28 @@
package tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUp_20220809091020(t *testing.T) {
db := applyUpToPrev(t)
applyNext(t, db)
stmt := `INSERT INTO windows_updates (host_id, date_epoch, kb_id) VALUES (?, ?, ?)`
_, err := db.Exec(stmt, 1, 1, 123)
require.NoError(t, err)
// This should raise an error
_, err = db.Exec(stmt, 1, 1, 123)
require.Error(t, err)
// Test windows_updates has no duplicates
var n uint
err = db.QueryRow(`SELECT COUNT(1) FROM windows_updates WHERE host_id=1`).Scan(&n)
require.NoError(t, err)
require.Equal(t, uint(1), n)
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,55 @@
package mysql
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
)
// InsertWindowsUpdates inserts one or more windows updates for the given host.
func (ds *Datastore) InsertWindowsUpdates(ctx context.Context, hostID uint, updates []fleet.WindowsUpdate) error {
if len(updates) == 0 {
return nil
}
// The windows_updates_history table in OSQUERY is append only so we only need to figure what
// new updates were installed since the last sync.
var lastUpdateEpoch uint
var args []interface{}
var placeholders []string
lastUpdateSmt := `SELECT date_epoch FROM windows_updates WHERE host_id = ? ORDER BY date_epoch DESC LIMIT 1`
if err := sqlx.GetContext(ctx, ds.reader, &lastUpdateEpoch, lastUpdateSmt, hostID); err != nil {
if err != sql.ErrNoRows {
return ctxerr.Wrap(ctx, err, "inserting windows updates")
}
lastUpdateEpoch = 0
}
for _, v := range updates {
if v.DateEpoch > lastUpdateEpoch {
placeholders = append(placeholders, "(?,?,?)")
args = append(args, hostID, v.DateEpoch, v.KBID)
}
}
if len(args) > 0 {
smt := fmt.Sprintf(
`INSERT IGNORE INTO windows_updates (host_id, date_epoch, kb_id) VALUES %s`,
strings.Join(placeholders, ","),
)
if _, err := ds.writer.ExecContext(ctx, smt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "inserting windows updates")
}
}
return nil
}

View File

@ -0,0 +1,73 @@
package mysql
import (
"context"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
)
func TestWindowsUpdates(t *testing.T) {
ds := CreateMySQLDS(t)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"InsertWindowsUpdates", testInsertWindowsUpdates},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testInsertWindowsUpdates(t *testing.T, ds *Datastore) {
ctx := context.Background()
now := uint(time.Now().Unix())
smt := `SELECT kb_id, date_epoch FROM windows_updates WHERE host_id = ?`
t.Run("with no stored updates", func(t *testing.T) {
hostID := 1
updates := []fleet.WindowsUpdate{
{KBID: 1, DateEpoch: now},
{KBID: 2, DateEpoch: now + 1},
}
err := ds.InsertWindowsUpdates(ctx, 1, updates)
require.NoError(t, err)
var actual []fleet.WindowsUpdate
err = sqlx.SelectContext(ctx, ds.reader, &actual, smt, hostID)
require.NoError(t, err)
require.ElementsMatch(t, updates, actual)
})
t.Run("with stored updates", func(t *testing.T) {
hostID := 1
updates := []fleet.WindowsUpdate{
{KBID: 1, DateEpoch: now},
{KBID: 2, DateEpoch: now + 1},
}
err := ds.InsertWindowsUpdates(ctx, 1, updates)
require.NoError(t, err)
updates = append(updates, fleet.WindowsUpdate{KBID: 3, DateEpoch: now + 2})
err = ds.InsertWindowsUpdates(ctx, 1, updates)
require.NoError(t, err)
var actual []fleet.WindowsUpdate
err = sqlx.SelectContext(ctx, ds.reader, &actual, smt, hostID)
require.NoError(t, err)
require.ElementsMatch(t, updates, actual)
})
}

View File

@ -473,13 +473,14 @@ type UpdateIntervalConfig struct {
// config file), not to be confused with VulnerabilitySettings which is the
// configuration in AppConfig.
type VulnerabilitiesConfig struct {
DatabasesPath string `json:"databases_path"`
Periodicity time.Duration `json:"periodicity"`
CPEDatabaseURL string `json:"cpe_database_url"`
CVEFeedPrefixURL string `json:"cve_feed_prefix_url"`
CurrentInstanceChecks string `json:"current_instance_checks"`
DisableDataSync bool `json:"disable_data_sync"`
RecentVulnerabilityMaxAge time.Duration `json:"recent_vulnerability_max_age"`
DatabasesPath string `json:"databases_path"`
Periodicity time.Duration `json:"periodicity"`
CPEDatabaseURL string `json:"cpe_database_url"`
CVEFeedPrefixURL string `json:"cve_feed_prefix_url"`
CurrentInstanceChecks string `json:"current_instance_checks"`
DisableDataSync bool `json:"disable_data_sync"`
RecentVulnerabilityMaxAge time.Duration `json:"recent_vulnerability_max_age"`
DisableWinOSVulnerabilities bool `json:"disable_win_os_vulnerabilities"`
}
type LoggingPlugin struct {

View File

@ -607,6 +607,10 @@ type Datastore interface {
InnoDBStatus(ctx context.Context) (string, error)
ProcessList(ctx context.Context) ([]MySQLProcess, error)
///////////////////////////////////////////////////////////////////////////////
// Windows Update History
InsertWindowsUpdates(ctx context.Context, hostID uint, updates []WindowsUpdate) error
}
const (

View File

@ -0,0 +1,78 @@
package fleet
import (
"fmt"
"regexp"
"strconv"
)
type WindowsUpdate struct {
KBID uint `db:"kb_id"`
DateEpoch uint `db:"date_epoch"`
}
// NewWindowsUpdate returns a new WindowsUpdate from the provided props:
// - title: The title of the windows update (see
// https://osquery.io/schema/5.4.0/#windows_update_history)
// - dateEpoch: The date the update was applied on (see
// https://osquery.io/schema/5.4.0/#windows_update_history)
func NewWindowsUpdate(title string, dateEpoch string) (WindowsUpdate, error) {
kbID, err := parseKBID(title)
if err != nil {
return WindowsUpdate{}, err
}
dEpoch, err := parseDateEpoch(dateEpoch)
if err != nil {
return WindowsUpdate{}, err
}
return WindowsUpdate{
KBID: kbID,
DateEpoch: dEpoch,
}, nil
}
func (wu WindowsUpdate) MoreRecent(other WindowsUpdate) bool {
return wu.DateEpoch > other.DateEpoch
}
func parseDateEpoch(val string) (uint, error) {
dEpoch, err := strconv.Atoi(val)
if err != nil {
return 0, err
}
if dEpoch < 0 {
return 0, fmt.Errorf("invalid epoch value %d", dEpoch)
}
return uint(dEpoch), nil
}
// parseKBID extracts the KB (Knowledge Base) id contained inside a string. KB ids are found based on
// the pattern 'KB\d+'. In case of multiple matches, the id
// will be based on the last match. Will return an error if:
// - No matches are found
// - The matched KB contains an 'invalid' id (< 0)
func parseKBID(str string) (uint, error) {
r := regexp.MustCompile(`\s?\(?KB(?P<Id>\d+)\s?\)?`)
m := r.FindAllStringSubmatch(str, -1)
idx := r.SubexpIndex("Id")
if len(m) == 0 || idx <= 0 {
return 0, fmt.Errorf("KB id not found in %s", str)
}
last := m[len(m)-1]
id, err := strconv.Atoi(last[idx])
if err != nil {
return 0, err
}
if id <= 0 {
return 0, fmt.Errorf("Invalid KB id value found in %s", str)
}
return uint(id), nil
}

View File

@ -0,0 +1,57 @@
package fleet
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseKBId(t *testing.T) {
testCases := []struct {
input string
expected uint
errors bool
}{
{
input: "2022-04 Update for Windows 10 Version 21H2 for x64-based Systems based on (KB2267602) based on KB2267601 (KB5005463)",
expected: 5005463,
errors: false,
},
{
input: "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.1239.0)",
expected: 2267602,
errors: false,
},
{
input: "2022-04 Update for Windows 10 Version 21H2 for x64-based Systems (KB5005463)",
expected: 5005463,
errors: false,
},
{
input: "2022-04 Update for Windows 10 Version 21H2 for x64-based Systems (KB-5005463)",
expected: 0,
errors: true,
},
{
input: "2022-04 Update for Windows 10 Version 21H2 for x64-based Systems (KB0)",
expected: 0,
errors: true,
},
{
input: "Some random string",
expected: 0,
errors: true,
},
}
for _, tCase := range testCases {
actual, err := parseKBID(tCase.input)
require.Equal(t, tCase.expected, actual)
if !tCase.errors {
require.NoError(t, err)
} else {
require.Error(t, err)
}
}
}

View File

@ -435,6 +435,8 @@ type InnoDBStatusFunc func(ctx context.Context) (string, error)
type ProcessListFunc func(ctx context.Context) ([]fleet.MySQLProcess, error)
type InsertWindowsUpdatesFunc func(ctx context.Context, hostID uint, updates []fleet.WindowsUpdate) error
type DataStore struct {
HealthCheckFunc HealthCheckFunc
HealthCheckFuncInvoked bool
@ -1068,6 +1070,9 @@ type DataStore struct {
ProcessListFunc ProcessListFunc
ProcessListFuncInvoked bool
InsertWindowsUpdatesFunc InsertWindowsUpdatesFunc
InsertWindowsUpdatesFuncInvoked bool
}
func (s *DataStore) HealthCheck() error {
@ -2124,3 +2129,8 @@ func (s *DataStore) ProcessList(ctx context.Context) ([]fleet.MySQLProcess, erro
s.ProcessListFuncInvoked = true
return s.ProcessListFunc(ctx)
}
func (s *DataStore) InsertWindowsUpdates(ctx context.Context, hostID uint, updates []fleet.WindowsUpdate) error {
s.InsertWindowsUpdatesFuncInvoked = true
return s.InsertWindowsUpdatesFunc(ctx, hostID, updates)
}

View File

@ -189,7 +189,7 @@ func TestAgentOptionsForHost(t *testing.T) {
// One of these queries is the disk space, only one of the two works in a platform. Similarly, one
// is for operating system.
var expectedDetailQueries = osquery_utils.GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{})
var expectedDetailQueries = osquery_utils.GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}})
func TestEnrollAgent(t *testing.T) {
ds := new(mock.Store)
@ -494,6 +494,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) {
hostDetailQueryPrefix + "orbit_info": {},
hostDetailQueryPrefix + "mdm": {},
hostDetailQueryPrefix + "munki_info": {},
hostDetailQueryPrefix + "windows_update_history": {},
}
for name := range queries {
require.NotEmpty(t, discovery[name])
@ -778,7 +779,8 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
require.NoError(t, err)
// -5 due to windows not having battery, mdm, munki_info and removed disk space query and
// operating system query (only 1 of 2 active for a given platform)
if !assert.Equal(t, len(expectedDetailQueries)-5, len(queries)) {
// -1 due to 'windows_update_history'
if !assert.Equal(t, len(expectedDetailQueries)-5, len(queries)-1) {
// this is just to print the diff between the expected and actual query
// keys when the count assertion fails, to help debugging - they are not
// expected to match.
@ -1382,8 +1384,8 @@ func TestDistributedQueryResults(t *testing.T) {
// Now we should get the active distributed query
queries, discovery, acc, err := svc.GetDistributedQueries(hostCtx)
require.NoError(t, err)
// -5 for the non-windows queries, +1 for the distributed query for campaign ID 42
if !assert.Equal(t, len(expectedDetailQueries)-4, len(queries)) {
// -3 for the non-windows queries, +1 for the distributed query for campaign ID 42
if !assert.Equal(t, len(expectedDetailQueries)-3, len(queries)) {
// this is just to print the diff between the expected and actual query
// keys when the count assertion fails, to help debugging - they are not
// expected to match.

View File

@ -380,7 +380,6 @@ var extraDetailQueries = map[string]DetailQuery{
Platforms: append(fleet.HostLinuxOSs, "darwin"),
DirectIngestFunc: directIngestOSUnixLike,
},
OrbitInfoQueryName: OrbitInfoDetailQuery,
}
@ -408,6 +407,13 @@ func withCachedUsers(query string) string {
return fmt.Sprintf(query, usersQueryStr)
}
var windowsUpdateHistory = DetailQuery{
Query: `SELECT date, title FROM windows_update_history WHERE result_code = 'Succeeded'`,
Platforms: []string{"windows"},
Discovery: discoveryTable("windows_update_history"),
DirectIngestFunc: directIngestWindowsUpdateHistory,
}
var softwareMacOS = DetailQuery{
// Note that we create the cached_users CTE (the WITH clause) in order to suggest to SQLite
// that it generates the users once instead of once for each UNIONed query. We use CROSS JOIN to
@ -761,6 +767,46 @@ func directIngestBattery(ctx context.Context, logger log.Logger, host *fleet.Hos
return ds.ReplaceHostBatteries(ctx, host.ID, mapping)
}
func directIngestWindowsUpdateHistory(
ctx context.Context,
logger log.Logger,
host *fleet.Host,
ds fleet.Datastore,
rows []map[string]string,
failed bool,
) error {
if failed {
level.Error(logger).Log("op", "directIngestWindowsUpdateHistory", "err", "failed")
return nil
}
// The windows update history table will also contain entries for the Defender Antivirus. Unfortunately
// there's no reliable way to differentiate between those entries and Cumulative OS updates.
// Since each antivirus update will have the same KB ID, but different 'dates', to
// avoid trying to insert duplicated data, we group by KB ID and then take the most 'out of
// date' update in each group.
uniq := make(map[uint]fleet.WindowsUpdate)
for _, row := range rows {
u, err := fleet.NewWindowsUpdate(row["title"], row["date"])
if err != nil {
level.Warn(logger).Log("op", "directIngestWindowsUpdateHistory", "skipped", err)
continue
}
if v, ok := uniq[u.KBID]; !ok || v.MoreRecent(u) {
uniq[u.KBID] = u
}
}
var updates []fleet.WindowsUpdate
for _, v := range uniq {
updates = append(updates, v)
}
return ds.InsertWindowsUpdates(ctx, host.ID, updates)
}
func directIngestOrbitInfo(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string, failed bool) error {
if len(rows) != 1 {
return ctxerr.Errorf(ctx, "invalid number of orbit_info rows: %d", len(rows))
@ -1032,6 +1078,10 @@ func GetDetailQueries(ac *fleet.AppConfig, fleetConfig config.FleetConfig) map[s
generatedMap["users"] = usersQuery
}
if !fleetConfig.Vulnerabilities.DisableWinOSVulnerabilities {
generatedMap["windows_update_history"] = windowsUpdateHistory
}
if fleetConfig.App.EnableScheduledQueryStats {
generatedMap["scheduled_query_stats"] = scheduledQueryStats
}

View File

@ -297,7 +297,8 @@ func sortedKeysCompare(t *testing.T, m map[string]DetailQuery, expectedKeys []st
func TestGetDetailQueries(t *testing.T) {
queriesNoConfig := GetDetailQueries(nil, config.FleetConfig{})
require.Len(t, queriesNoConfig, 15)
require.Len(t, queriesNoConfig, 16)
baseQueries := []string{
"network_interface",
"os_version",
@ -314,15 +315,19 @@ func TestGetDetailQueries(t *testing.T) {
"battery",
"os_windows",
"os_unix_like",
"windows_update_history",
}
sortedKeysCompare(t, queriesNoConfig, baseQueries)
queriesWithoutWinOSVuln := GetDetailQueries(nil, config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}})
require.Len(t, queriesWithoutWinOSVuln, 15)
queriesWithUsers := GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}})
require.Len(t, queriesWithUsers, 17)
require.Len(t, queriesWithUsers, 18)
sortedKeysCompare(t, queriesWithUsers, append(baseQueries, "users", "scheduled_query_stats"))
queriesWithUsersAndSoftware := GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}}, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}})
require.Len(t, queriesWithUsersAndSoftware, 20)
require.Len(t, queriesWithUsersAndSoftware, 21)
sortedKeysCompare(t, queriesWithUsersAndSoftware,
append(baseQueries, "users", "software_macos", "software_linux", "software_windows", "scheduled_query_stats"))
}
@ -712,3 +717,42 @@ func TestDirectIngestSoftware(t *testing.T) {
}
})
}
func TestDirectIngestWindowsUpdateHistory(t *testing.T) {
ds := new(mock.Store)
ds.InsertWindowsUpdatesFunc = func(ctx context.Context, hostID uint, updates []fleet.WindowsUpdate) error {
require.Len(t, updates, 6)
require.ElementsMatch(t, []fleet.WindowsUpdate{
{KBID: 2267602, DateEpoch: 1657929207},
{KBID: 890830, DateEpoch: 1658226954},
{KBID: 5013887, DateEpoch: 1658225364},
{KBID: 5005463, DateEpoch: 1658225225},
{KBID: 5010472, DateEpoch: 1658224963},
{KBID: 4052623, DateEpoch: 1657929544},
}, updates)
return nil
}
host := fleet.Host{
ID: 1,
}
payload := []map[string]string{
{"date": "1659392951", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.1239.0)"},
{"date": "1658271402", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.442.0)"},
{"date": "1658228495", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.415.0)"},
{"date": "1658226954", "title": "Windows Malicious Software Removal Tool x64 - v5.103 (KB890830)"},
{"date": "1658225364", "title": "2022-06 Cumulative Update for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5013887)"},
{"date": "1658225225", "title": "2022-04 Update for Windows 10 Version 21H2 for x64-based Systems (KB5005463)"},
{"date": "1658224963", "title": "2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5010472)"},
{"date": "1658222131", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.400.0)"},
{"date": "1658189063", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.376.0)"},
{"date": "1658185542", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.386.0)"},
{"date": "1657929544", "title": "Update for Microsoft Defender Antivirus antimalware platform - KB4052623 (Version 4.18.2205.7)"},
{"date": "1657929207", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.203.0)"},
}
err := directIngestWindowsUpdateHistory(context.Background(), log.NewNopLogger(), &host, ds, payload, false)
require.NoError(t, err)
require.True(t, ds.InsertWindowsUpdatesFuncInvoked)
}

View File

@ -118,13 +118,14 @@ func (svc *Service) UpdateIntervalConfig(ctx context.Context) (*fleet.UpdateInte
func (svc *Service) VulnerabilitiesConfig(ctx context.Context) (*fleet.VulnerabilitiesConfig, error) {
return &fleet.VulnerabilitiesConfig{
DatabasesPath: svc.config.Vulnerabilities.DatabasesPath,
Periodicity: svc.config.Vulnerabilities.Periodicity,
CPEDatabaseURL: svc.config.Vulnerabilities.CPEDatabaseURL,
CVEFeedPrefixURL: svc.config.Vulnerabilities.CVEFeedPrefixURL,
CurrentInstanceChecks: svc.config.Vulnerabilities.CurrentInstanceChecks,
DisableDataSync: svc.config.Vulnerabilities.DisableDataSync,
RecentVulnerabilityMaxAge: svc.config.Vulnerabilities.RecentVulnerabilityMaxAge,
DatabasesPath: svc.config.Vulnerabilities.DatabasesPath,
Periodicity: svc.config.Vulnerabilities.Periodicity,
CPEDatabaseURL: svc.config.Vulnerabilities.CPEDatabaseURL,
CVEFeedPrefixURL: svc.config.Vulnerabilities.CVEFeedPrefixURL,
CurrentInstanceChecks: svc.config.Vulnerabilities.CurrentInstanceChecks,
DisableDataSync: svc.config.Vulnerabilities.DisableDataSync,
RecentVulnerabilityMaxAge: svc.config.Vulnerabilities.RecentVulnerabilityMaxAge,
DisableWinOSVulnerabilities: svc.config.Vulnerabilities.DisableWinOSVulnerabilities,
}, nil
}