From 8fdfb9a004a5cb6f69ce4840c8dfdbcb61766731 Mon Sep 17 00:00:00 2001 From: Sharon Katz <121527325+sharon-fdm@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:12:28 -0400 Subject: [PATCH] Add AI features, maintenance window, and Fleet Desktop to usage statistics Story #19693 (#21131) Additional statistics as described in https://github.com/fleetdm/fleet/pull/20091 : `aiFeaturesDisabled`: Whether server_settings.ai_features_disabled is set to true in the config. `maintenanceWindowsEnabled`: Whether at least one team has integrations.google_calendar.enable_calendar_events set to true `maintenanceWindowsConfigured`: Maintenance windows are considered "configured" if: configuration has value set for integrations.google_calendar[0].domain configuration has value set for integrations.google_calendar[0].api_key_json `numHostsFleetDesktopEnabled`: The number of hosts with Fleet desktop installed. --- changes/19693-additional-stats | 1 + cmd/fleet/serve_test.go | 6 +++++- docs/Using Fleet/Usage-statistics.md | 6 +++++- server/datastore/mysql/hosts.go | 12 ++++++++++++ server/datastore/mysql/statistics.go | 22 ++++++++++++++++++++++ server/datastore/mysql/statistics_test.go | 16 ++++++++++++++++ server/fleet/statistics.go | 10 ++++++++++ 7 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 changes/19693-additional-stats diff --git a/changes/19693-additional-stats b/changes/19693-additional-stats new file mode 100644 index 0000000000..5978543420 --- /dev/null +++ b/changes/19693-additional-stats @@ -0,0 +1 @@ +- Added additional statistics items for AI features, maintenance window, and Fleet Desktop diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 41e7d3885a..d8e6567b24 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -116,6 +116,10 @@ func TestMaybeSendStatistics(t *testing.T) { HostsEnrolledByOsqueryVersion: []fleet.HostsCountByOsqueryVersion{}, StoredErrors: []byte(`[]`), Organization: "Fleet", + AIFeaturesDisabled: true, + MaintenanceWindowsEnabled: true, + MaintenanceWindowsConfigured: true, + NumHostsFleetDesktopEnabled: 1984, }, true, nil } recorded := false @@ -134,7 +138,7 @@ func TestMaybeSendStatistics(t *testing.T) { require.NoError(t, err) assert.True(t, recorded) require.True(t, cleanedup) - assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numSoftwareVersions":100,"numHostSoftwares":101,"numSoftwareTitles":102,"numHostSoftwareInstalledPaths":103,"numSoftwareCPEs":104,"numSoftwareCVEs":105,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"mdmMacOsEnabled":false,"hostExpiryEnabled":false,"mdmWindowsEnabled":false,"liveQueryDisabled":false,"numWeeklyActiveUsers":111,"numWeeklyPolicyViolationDaysActual":0,"numWeeklyPolicyViolationDaysPossible":0,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"hostsEnrolledByOrbitVersion":[],"hostsEnrolledByOsqueryVersion":[],"storedErrors":[],"numHostsNotResponding":0}`, requestBody) + assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numSoftwareVersions":100,"numHostSoftwares":101,"numSoftwareTitles":102,"numHostSoftwareInstalledPaths":103,"numSoftwareCPEs":104,"numSoftwareCVEs":105,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"mdmMacOsEnabled":false,"hostExpiryEnabled":false,"mdmWindowsEnabled":false,"liveQueryDisabled":false,"numWeeklyActiveUsers":111,"numWeeklyPolicyViolationDaysActual":0,"numWeeklyPolicyViolationDaysPossible":0,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"hostsEnrolledByOrbitVersion":[],"hostsEnrolledByOsqueryVersion":[],"storedErrors":[],"numHostsNotResponding":0,"aiFeaturesDisabled":true,"maintenanceWindowsEnabled":true,"maintenanceWindowsConfigured":true,"numHostsFleetDesktopEnabled":1984}`, requestBody) } func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) { diff --git a/docs/Using Fleet/Usage-statistics.md b/docs/Using Fleet/Usage-statistics.md index 28413ee174..e546fbdd40 100644 --- a/docs/Using Fleet/Usage-statistics.md +++ b/docs/Using Fleet/Usage-statistics.md @@ -102,7 +102,11 @@ Below is the JSON payload that is sent to Fleet Device Management Inc: }, ... ], - "numHostsNotResponding": 9 + "numHostsNotResponding": 9, + "aiFeaturesDisabled": true, + "maintenanceWindowsEnabled": true, + "maintenanceWindowsConfigured": true, + "numHostsFleetDesktopEnabled": 999 } ``` diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 4a9ef47c7c..009d9b832c 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -5035,6 +5035,18 @@ func amountHostsByOsqueryVersionDB(ctx context.Context, db sqlx.QueryerContext) return counts, nil } +func numHostsFleetDesktopEnabledDB(ctx context.Context, db sqlx.QueryerContext) (int, error) { + var count int + const stmt = ` + SELECT count(*) FROM host_orbit_info WHERE desktop_version IS NOT NULL + ` + if err := sqlx.GetContext(ctx, db, &count, stmt); err != nil { + return 0, err + } + + return count, nil +} + func (ds *Datastore) GetMatchingHostSerials(ctx context.Context, serials []string) (map[string]*fleet.Host, error) { result := map[string]*fleet.Host{} if len(serials) == 0 { diff --git a/server/datastore/mysql/statistics.go b/server/datastore/mysql/statistics.go index 55e9b9e4ee..31b65429fe 100644 --- a/server/datastore/mysql/statistics.go +++ b/server/datastore/mysql/statistics.go @@ -10,6 +10,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/version" "github.com/go-kit/log/level" "github.com/jmoiron/sqlx" @@ -98,6 +99,10 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du if err != nil { return ctxerr.Wrap(ctx, err, "amount hosts by osquery version") } + numHostsFleetDesktopEnabled, err := numHostsFleetDesktopEnabledDB(ctx, ds.reader(ctx)) + if err != nil { + return ctxerr.Wrap(ctx, err, "number of hosts with Fleet desktop installed") + } stats.NumHostsEnrolled = amountEnrolledHosts stats.NumUsers = amountUsers @@ -130,6 +135,23 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du if lic != nil && lic.IsPremium() { stats.Organization = lic.Organization } + stats.AIFeaturesDisabled = appConfig.ServerSettings.AIFeaturesDisabled + stats.MaintenanceWindowsConfigured = len(appConfig.Integrations.GoogleCalendar) > 0 && appConfig.Integrations.GoogleCalendar[0].Domain != "" && len(appConfig.Integrations.GoogleCalendar[0].ApiKey) > 0 + + stats.MaintenanceWindowsEnabled = false + teams, err := ds.ListTeams(ctx, fleet.TeamFilter{User: &fleet.User{ + GlobalRole: ptr.String(fleet.RoleAdmin), + }}, fleet.ListOptions{}) + if err != nil { + return ctxerr.Wrap(ctx, err, "list teams") + } + for _, team := range teams { + if team.Config.Integrations.GoogleCalendar != nil && team.Config.Integrations.GoogleCalendar.Enable { + stats.MaintenanceWindowsEnabled = true + break + } + } + stats.NumHostsFleetDesktopEnabled = numHostsFleetDesktopEnabled return nil } diff --git a/server/datastore/mysql/statistics_test.go b/server/datastore/mysql/statistics_test.go index 0f8e5b426a..c152e03429 100644 --- a/server/datastore/mysql/statistics_test.go +++ b/server/datastore/mysql/statistics_test.go @@ -87,6 +87,10 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { assert.Equal(t, false, stats.HostExpiryEnabled) assert.Equal(t, false, stats.MDMWindowsEnabled) assert.Equal(t, false, stats.LiveQueryDisabled) + assert.Equal(t, false, stats.AIFeaturesDisabled) + assert.Equal(t, false, stats.MaintenanceWindowsEnabled) + assert.Equal(t, false, stats.MaintenanceWindowsConfigured) + assert.Equal(t, 0, stats.NumHostsFleetDesktopEnabled) firstIdentifier := stats.AnonymousIdentifier @@ -227,6 +231,10 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { assert.Equal(t, `[{"count":10,"loc":["a","b","c"]}]`, string(stats.StoredErrors)) assert.Equal(t, []fleet.HostsCountByOsqueryVersion{{OsqueryVersion: "4.9.0", NumHosts: 1}}, stats.HostsEnrolledByOsqueryVersion) assert.Equal(t, []fleet.HostsCountByOrbitVersion{{OrbitVersion: "1.1.0", NumHosts: 1}}, stats.HostsEnrolledByOrbitVersion) + assert.Equal(t, false, stats.AIFeaturesDisabled) + assert.Equal(t, false, stats.MaintenanceWindowsEnabled) + assert.Equal(t, false, stats.MaintenanceWindowsConfigured) + assert.Equal(t, 1, stats.NumHostsFleetDesktopEnabled) err = ds.RecordStatisticsSent(ctx) require.NoError(t, err) @@ -332,6 +340,10 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { {Version: "", NumEnrolled: 1}, }, stats.HostsEnrolledByOperatingSystem[""]) assert.Equal(t, `[{"count":10,"loc":["a","b","c"]}]`, string(stats.StoredErrors)) + assert.Equal(t, false, stats.AIFeaturesDisabled) + assert.Equal(t, false, stats.MaintenanceWindowsEnabled) + assert.Equal(t, false, stats.MaintenanceWindowsConfigured) + assert.Equal(t, 1, stats.NumHostsFleetDesktopEnabled) // Create multiple new sessions for a single user _, err = ds.NewSession(ctx, u1.ID, "session_key2") @@ -366,6 +378,10 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { assert.Equal(t, 0, stats.NumWeeklyPolicyViolationDaysActual) assert.Equal(t, 0, stats.NumWeeklyPolicyViolationDaysPossible) assert.Equal(t, `[{"count":10,"loc":["a","b","c"]}]`, string(stats.StoredErrors)) + assert.Equal(t, false, stats.AIFeaturesDisabled) + assert.Equal(t, false, stats.MaintenanceWindowsEnabled) + assert.Equal(t, false, stats.MaintenanceWindowsConfigured) + assert.Equal(t, 1, stats.NumHostsFleetDesktopEnabled) // Add host to test hosts not responding stats _, err = ds.NewHost(ctx, &fleet.Host{ diff --git a/server/fleet/statistics.go b/server/fleet/statistics.go index 11a7feeb66..0d88d52def 100644 --- a/server/fleet/statistics.go +++ b/server/fleet/statistics.go @@ -47,6 +47,16 @@ type StatisticsPayload struct { StoredErrors json.RawMessage `json:"storedErrors"` // NumHostsNotResponding is a count of hosts that connect to Fleet successfully but fail to submit results for distributed queries. NumHostsNotResponding int `json:"numHostsNotResponding"` + // Whether server_settings.ai_features_disabled is set to true in the config. + AIFeaturesDisabled bool `json:"aiFeaturesDisabled"` + // Whether at least one team has integrations.google_calendar.enable_calendar_events set to true + MaintenanceWindowsEnabled bool `json:"maintenanceWindowsEnabled"` + // Maintenance windows are considered "configured" if: + // configuration has value set for integrations.google_calendar[0].domain + // configuration has value set for integrations.google_calendar[0].api_key_json + MaintenanceWindowsConfigured bool `json:"maintenanceWindowsConfigured"` + // The number of hosts with Fleet desktop installed. + NumHostsFleetDesktopEnabled int `json:"numHostsFleetDesktopEnabled"` } type HostsCountByOrbitVersion struct {