mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
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:
parent
55af48910a
commit
3048a07fd1
3
changes/feature-7076-store-windows-updates
Normal file
3
changes/feature-7076-store-windows-updates
Normal 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.
|
@ -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,
|
||||
|
@ -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",
|
||||
|
1058
cmd/osquery-perf/windows_11.tmpl
Normal file
1058
cmd/osquery-perf/windows_11.tmpl
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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
55
server/datastore/mysql/windows_updates.go
Normal file
55
server/datastore/mysql/windows_updates.go
Normal 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
|
||||
}
|
73
server/datastore/mysql/windows_updates_test.go
Normal file
73
server/datastore/mysql/windows_updates_test.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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 {
|
||||
|
@ -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 (
|
||||
|
78
server/fleet/windows_updates.go
Normal file
78
server/fleet/windows_updates.go
Normal 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
|
||||
}
|
57
server/fleet/windows_updates_tests.go
Normal file
57
server/fleet/windows_updates_tests.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
BIN
server/vulnerabilities/testdata/windows/software/windows_11-software.json.bz2
vendored
Normal file
BIN
server/vulnerabilities/testdata/windows/software/windows_11-software.json.bz2
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user