fleet/server/datastore/mysql/statistics_test.go
Ian Littman 2ef729e473
Allow opting in users to email verification on login (#24273)
#22790 

Changes file is on the FE PR.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality
2024-12-05 08:37:10 -06:00

428 lines
16 KiB
Go

package mysql
import (
"context"
"database/sql"
"encoding/json"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/config"
"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/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStatistics(t *testing.T) {
ds := CreateMySQLDS(t)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"ShouldSend", testStatisticsShouldSend},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
eh := ctxerr.MockHandler{}
// Mock the error handler to always return an error
eh.RetrieveImpl = func(flush bool) ([]*ctxerr.StoredError, error) {
require.False(t, flush)
return []*ctxerr.StoredError{
{Count: 10, Chain: json.RawMessage(`[{"stack": ["a","b","c","d"]}]`)},
}, nil
}
ctxb := context.Background()
ctx := ctxerr.NewContext(ctxb, eh)
fleetConfig := config.FleetConfig{Osquery: config.OsqueryConfig{DetailUpdateInterval: 1 * time.Hour}}
premiumLicense := &fleet.LicenseInfo{Tier: fleet.TierPremium, Organization: "Fleet"}
freeLicense := &fleet.LicenseInfo{Tier: fleet.TierFree}
var builtinLabels int
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &builtinLabels, `SELECT COUNT(*) FROM labels`)
})
// First time running with no hosts
stats, shouldSend, err := ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, "premium", stats.LicenseTier)
assert.Equal(t, "Fleet", stats.Organization)
assert.Equal(t, 0, stats.NumHostsEnrolled)
assert.Equal(t, 0, stats.NumUsers)
assert.Equal(t, 0, stats.NumSoftwareVersions)
assert.Equal(t, 0, stats.NumHostSoftwares)
assert.Equal(t, 0, stats.NumSoftwareTitles)
assert.Equal(t, 0, stats.NumHostSoftwareInstalledPaths)
assert.Equal(t, 0, stats.NumSoftwareCPEs)
assert.Equal(t, 0, stats.NumSoftwareCVEs)
assert.Equal(t, 0, stats.NumTeams)
assert.Equal(t, 0, stats.NumPolicies)
assert.Equal(t, 0, stats.NumQueries)
assert.Equal(t, builtinLabels, stats.NumLabels)
assert.Equal(t, false, stats.SoftwareInventoryEnabled)
assert.Equal(t, true, stats.SystemUsersEnabled)
assert.Equal(t, false, stats.VulnDetectionEnabled)
assert.Equal(t, false, stats.HostsStatusWebHookEnabled)
assert.Equal(t, 0, stats.NumWeeklyActiveUsers)
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, []fleet.HostsCountByOsqueryVersion{}, stats.HostsEnrolledByOsqueryVersion) // should be empty slice instead of nil
assert.Equal(t, []fleet.HostsCountByOrbitVersion{}, stats.HostsEnrolledByOrbitVersion) // should be empty slice instead of nil
assert.Equal(t, false, stats.MDMMacOsEnabled)
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
err = ds.RecordStatisticsSent(ctx)
require.NoError(t, err)
// Create new host for test
h1, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("1"),
UUID: "1",
Hostname: "foo.local",
PrimaryIP: "192.168.1.1",
PrimaryMac: "30-65-EC-6F-C4-58",
OsqueryHostID: ptr.String("M"),
OsqueryVersion: "4.9.0",
})
require.NoError(t, err)
// Create host_orbit_info record for test
require.NoError(
t, ds.SetOrUpdateHostOrbitInfo(
ctx, h1.ID, "1.1.0", sql.NullString{String: "1.1.0", Valid: true}, sql.NullBool{Bool: true, Valid: true},
),
)
// Create two new users for test
u1, err := ds.NewUser(ctx, &fleet.User{
Password: []byte("foobar"),
AdminForcedPasswordReset: false,
Email: "baz@example.com",
SSOEnabled: false,
GlobalRole: ptr.String(fleet.RoleObserver),
})
require.NoError(t, err)
_, err = ds.NewUser(ctx, &fleet.User{
Password: []byte("foobar"),
AdminForcedPasswordReset: false,
Email: "qux@example.com",
SSOEnabled: false,
GlobalRole: ptr.String(fleet.RoleObserver),
})
require.NoError(t, err)
// Create a session for user baz, but not qux (so only 1 is active)
_, err = ds.NewSession(ctx, u1.ID, 8)
require.NoError(t, err)
// Create new team for test
_, err = ds.NewTeam(ctx, &fleet.Team{
Name: "footeam",
Description: "team of foo",
})
require.NoError(t, err)
// Create new global policy for test
_, err = ds.NewGlobalPolicy(ctx, ptr.Uint(1), fleet.PolicyPayload{
Name: "testpolicy",
Query: "select 1;",
Description: "test policy desc",
Resolution: "test policy resolution",
})
require.NoError(t, err)
// Create new label for test
_, err = ds.NewLabel(ctx, &fleet.Label{
Name: "testlabel",
Query: "select 1;",
Platform: "darwin",
Description: "test label description",
})
require.NoError(t, err)
// Create new app cfg for test
cfg, err := ds.NewAppConfig(ctx, &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{
OrgName: "Test",
OrgLogoURL: "localhost:8080/logo.png",
},
})
require.NoError(t, err)
// Initialize policy violation days for test
pvdJSON, err := json.Marshal(PolicyViolationDays{FailingHostCount: 5, TotalHostCount: 10})
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO
aggregated_stats (id, global_stats, type, json_value, created_at, updated_at)
VALUES (?, ?, ?, CAST(? AS JSON), ?, ?)
ON DUPLICATE KEY UPDATE
json_value = VALUES(json_value),
updated_at = VALUES(updated_at)
`, 0, true, aggregatedStatsTypePolicyViolationsDays, pvdJSON, time.Now().Add(-48*time.Hour), time.Now().Add(-7*24*time.Hour))
require.NoError(t, err)
require.NoError(t, err)
cfg.Features.EnableSoftwareInventory = false
cfg.Features.EnableHostUsers = false
cfg.VulnerabilitySettings.DatabasesPath = ""
cfg.WebhookSettings.HostStatusWebhook.Enable = true
cfg.MDM.EnabledAndConfigured = true
cfg.HostExpirySettings.HostExpiryEnabled = true
cfg.MDM.WindowsEnabledAndConfigured = true
cfg.ServerSettings.LiveQueryDisabled = true
err = ds.SaveAppConfig(ctx, cfg)
require.NoError(t, err)
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
// Running with 1 host
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.NotEmpty(t, stats.AnonymousIdentifier)
assert.NotEmpty(t, stats.FleetVersion)
assert.Equal(t, "premium", stats.LicenseTier)
assert.Equal(t, "Fleet", stats.Organization)
assert.Equal(t, 1, stats.NumHostsEnrolled)
assert.Equal(t, 2, stats.NumUsers)
assert.Equal(t, 0, stats.NumSoftwareVersions)
assert.Equal(t, 0, stats.NumHostSoftwares)
assert.Equal(t, 0, stats.NumSoftwareTitles)
assert.Equal(t, 0, stats.NumHostSoftwareInstalledPaths)
assert.Equal(t, 0, stats.NumSoftwareCPEs)
assert.Equal(t, 0, stats.NumSoftwareCVEs)
assert.Equal(t, 1, stats.NumTeams)
assert.Equal(t, 1, stats.NumPolicies)
assert.Equal(t, 0, stats.NumQueries)
assert.Equal(t, builtinLabels+1, stats.NumLabels)
assert.Equal(t, false, stats.SoftwareInventoryEnabled)
assert.Equal(t, false, stats.SystemUsersEnabled)
assert.Equal(t, false, stats.VulnDetectionEnabled)
assert.Equal(t, true, stats.HostsStatusWebHookEnabled)
assert.Equal(t, 1, stats.NumWeeklyActiveUsers)
assert.Equal(t, 5, stats.NumWeeklyPolicyViolationDaysActual)
assert.Equal(t, 10, stats.NumWeeklyPolicyViolationDaysPossible)
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)
// If we try right away, it shouldn't ask to send
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), fleet.StatisticsFrequency, fleetConfig)
require.NoError(t, err)
assert.False(t, shouldSend)
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
// create a few more hosts, with platforms and os versions
_, err = ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("2"),
UUID: "2",
Hostname: "foo.local2",
PrimaryIP: "192.168.1.2",
PrimaryMac: "30-65-EC-6F-C4-59",
OsqueryHostID: ptr.String("S"),
Platform: "rhel",
OSVersion: "Fedora 35",
})
require.NoError(t, err)
_, err = ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("3"),
UUID: "3",
Hostname: "foo.local3",
PrimaryIP: "192.168.1.3",
PrimaryMac: "40-65-EC-6F-C4-59",
OsqueryHostID: ptr.String("T"),
Platform: "rhel",
OSVersion: "Fedora 35",
})
require.NoError(t, err)
_, err = ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("4"),
UUID: "4",
Hostname: "foo.local4",
PrimaryIP: "192.168.1.4",
PrimaryMac: "50-65-EC-6F-C4-59",
OsqueryHostID: ptr.String("U"),
Platform: "macos",
OSVersion: "10.11.12",
})
require.NoError(t, err)
_, err = ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("5"),
UUID: "5",
Hostname: "foo.local5",
PrimaryIP: "192.168.1.5",
PrimaryMac: "60-65-EC-6F-C4-59",
OsqueryHostID: ptr.String("V"),
Platform: "rhel",
OSVersion: "Fedora 36",
})
require.NoError(t, err)
// Lower the frequency to trigger an "outdated" sent
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, "premium", stats.LicenseTier)
assert.Equal(t, "Fleet", stats.Organization)
assert.Equal(t, 5, stats.NumHostsEnrolled)
assert.Equal(t, 2, stats.NumUsers)
assert.Equal(t, 0, stats.NumQueries)
assert.Equal(t, 0, stats.NumSoftwareVersions)
assert.Equal(t, 0, stats.NumHostSoftwares)
assert.Equal(t, 0, stats.NumSoftwareTitles)
assert.Equal(t, 0, stats.NumHostSoftwareInstalledPaths)
assert.Equal(t, 0, stats.NumSoftwareCPEs)
assert.Equal(t, 0, stats.NumSoftwareCVEs)
assert.Equal(t, 0, stats.NumWeeklyActiveUsers) // no active user since last stats were sent
require.Len(t, stats.HostsEnrolledByOperatingSystem, 3) // empty platform, rhel and macos
assert.Equal(t, 5, stats.NumWeeklyPolicyViolationDaysActual)
require.ElementsMatch(t, []fleet.HostsCountByOSVersion{
{Version: "Fedora 35", NumEnrolled: 2},
{Version: "Fedora 36", NumEnrolled: 1},
}, stats.HostsEnrolledByOperatingSystem["rhel"])
require.ElementsMatch(t, []fleet.HostsCountByOSVersion{
{Version: "10.11.12", NumEnrolled: 1},
}, stats.HostsEnrolledByOperatingSystem["macos"])
require.ElementsMatch(t, []fleet.HostsCountByOSVersion{
{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, 8)
require.NoError(t, err)
_, err = ds.NewSession(ctx, u1.ID, 8)
require.NoError(t, err)
_, err = ds.NewSession(ctx, u1.ID, 8)
require.NoError(t, err)
// CleanupStatistics resets policy violation days
err = ds.CleanupStatistics(ctx)
require.NoError(t, err)
// wait a bit and resend statistics
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, stats.AnonymousIdentifier, firstIdentifier)
assert.Equal(t, "premium", stats.LicenseTier)
assert.Equal(t, "Fleet", stats.Organization)
assert.Equal(t, 5, stats.NumHostsEnrolled)
assert.Equal(t, 2, stats.NumUsers)
assert.Equal(t, 0, stats.NumQueries)
assert.Equal(t, 0, stats.NumSoftwareVersions)
assert.Equal(t, 0, stats.NumHostSoftwares)
assert.Equal(t, 0, stats.NumSoftwareTitles)
assert.Equal(t, 0, stats.NumHostSoftwareInstalledPaths)
assert.Equal(t, 0, stats.NumSoftwareCPEs)
assert.Equal(t, 0, stats.NumSoftwareCVEs)
assert.Equal(t, 1, stats.NumWeeklyActiveUsers)
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{
DetailUpdatedAt: time.Now().Add(-3 * time.Hour),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("6"),
UUID: "6",
Hostname: "non-responsive.local",
PrimaryIP: "192.168.1.6",
PrimaryMac: "30-65-EC-6F-C4-66",
OsqueryHostID: ptr.String("NR"),
})
require.NoError(t, err)
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, "premium", stats.LicenseTier)
assert.Equal(t, "Fleet", stats.Organization)
assert.Equal(t, 6, stats.NumHostsEnrolled)
assert.Equal(t, 1, stats.NumHostsNotResponding)
// trigger again with a free license, organization should be "unknown"
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, freeLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, "free", stats.LicenseTier)
assert.Equal(t, "unknown", stats.Organization)
fleetConfig.Vulnerabilities = config.VulnerabilitiesConfig{DatabasesPath: "some/path/vulns"}
stats, shouldSend, err = ds.ShouldSendStatistics(license.NewContext(ctx, freeLicense), time.Millisecond, fleetConfig)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.True(t, stats.VulnDetectionEnabled)
}