Fleet Premium: de-anonymize usage stats (#7013)

This commit is contained in:
Martin Angers
2022-08-03 14:44:34 -04:00
committed by GitHub
parent ded5a3592e
commit 012869f971
14 changed files with 47 additions and 18 deletions
@@ -0,0 +1,2 @@
* Fleet Premium: De-anonymize usage statistics by adding an `organization` property to the usage statistics payload. For Fleet Free instances, organization is reported as "unknown". To turn off usage statistics in the Fleet UI, head to **Settings > Organization settings > Usage statistics**, de-select the "Enable usage statistics" checkbox, and select **Save**.
+2 -1
View File
@@ -62,6 +62,7 @@ func TestMaybeSendStatistics(t *testing.T) {
},
},
StoredErrors: []byte(`[]`),
Organization: "Fleet",
}, true, nil
}
recorded := false
@@ -73,7 +74,7 @@ func TestMaybeSendStatistics(t *testing.T) {
err := trySendStatistics(context.Background(), ds, fleet.StatisticsFrequency, ts.URL, fleetConfig, &fleet.LicenseInfo{Tier: "premium"})
require.NoError(t, err)
assert.True(t, recorded)
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","numHostsEnrolled":999,"numUsers":99,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"numWeeklyActiveUsers":111,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"storedErrors":[],"numHostsNotResponding":0}`, requestBody)
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"numWeeklyActiveUsers":111,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"storedErrors":[],"numHostsNotResponding":0}`, requestBody)
}
func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) {
+3 -3
View File
@@ -302,7 +302,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
return err
}
// disable anonymous analytics collection and enable software inventory for preview
// disable analytics collection and enable software inventory for preview
if err := client.ApplyAppConfig(map[string]map[string]bool{
"host_settings": {"enable_software_inventory": true},
"server_settings": {"enable_analytics": false},
@@ -319,12 +319,12 @@ Use the stop and reset subcommands to manage the server and dependencies once st
return errors.New("Expected 1 active enroll secret")
}
// disable anonymous analytics collection for preview
// disable analytics collection for preview
if err := client.ApplyAppConfig(map[string]map[string]bool{
"server_settings": {"enable_analytics": false},
},
); err != nil {
return fmt.Errorf("Error disabling anonymous analytics collection in app config: %w", err)
return fmt.Errorf("Error disabling analytics collection in app config: %w", err)
}
fmt.Println("Fleet will now log you into the UI automatically.")
+1 -1
View File
@@ -105,7 +105,7 @@ func setupCommand() *cli.Command {
return fmt.Errorf("error setting token for the current context: %w", err)
}
fmt.Println("Fleet Device Management Inc. periodically collects anonymous information about your instance.\nSending usage statistics from your Fleet instance is optional and can be disabled in settings.")
fmt.Println("Fleet Device Management Inc. periodically collects information about your instance.\nSending usage statistics from your Fleet instance is optional and can be disabled in settings.")
fmt.Println("[+] Fleet setup successful and context configured!")
return nil
+3 -2
View File
@@ -4,7 +4,7 @@
️ In Fleet 4.0, Usage statistics were introduced.
```
Fleet Device Management Inc. periodically collects anonymous information about your instance.
Fleet Device Management Inc. periodically collects information about your instance.
## What is included in usage statistics in Fleet?
@@ -15,6 +15,7 @@ Fleet Device Management Inc. periodically collects anonymous information about y
"anonymousIdentifier": "9pnzNmrES3mQG66UQtd29cYTiX2+fZ4CYxDvh495720=",
"fleetVersion": "x.x.x",
"licenseTier": "free",
"organization": "Fleet",
"numHostsEnrolled": 999,
"numUsers": 999,
"numTeams": 999,
@@ -89,7 +90,7 @@ Fleet Device Management Inc. periodically collects anonymous information about y
}
```
- All statistics are anonymous and contain no personal information about any particular device, organization, or person.
- Statistics contain no personal information about any particular device or person.
- Sending Usage statistics from your Fleet instance is optional and can be disabled.
@@ -98,9 +98,9 @@ const ConfirmationPage = ({
{importOsqueryConfig()}
</div>
<p>
Fleet Device Management Inc. periodically collects anonymous information
about your instance. Sending usage statistics from your Fleet instance
is optional and can be disabled in settings.
Fleet Device Management Inc. periodically collects information about
your instance. Sending usage statistics from your Fleet instance is
optional and can be disabled in settings.
</p>
<Button
type="submit"
@@ -87,7 +87,7 @@ const Statistics = ({
<div className={`${baseClass}__section`}>
<h2>Usage statistics</h2>
<p className={`${baseClass}__section-description`}>
Help improve Fleet by sending anonymous usage statistics.
Help improve Fleet by sending usage statistics.
<br />
<br />
This information helps our team better understand feature adoption
@@ -70,6 +70,7 @@ export const usageStatsPreview = {
anonymousIdentifier: "9pnzNmrES3mQG66UQtd29cYTiX2+fZ4CYxDvh495720=",
fleetVersion: "x.x.x",
licenseTier: "free",
organization: "Fleet",
numHostsEnrolled: 999,
numUsers: 999,
numTeams: 999,
+4
View File
@@ -70,6 +70,10 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
stats.HostsEnrolledByOperatingSystem = enrolledHostsByOS
stats.StoredErrors = storedErrs
stats.NumHostsNotResponding = amountHostsNotResponding
stats.Organization = "unknown"
if license.IsPremium() {
stats.Organization = license.Organization
}
return nil
}
+23 -6
View File
@@ -125,15 +125,17 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
license := &fleet.LicenseInfo{Tier: "premium"}
premiumLicense := &fleet.LicenseInfo{Tier: "premium", Organization: "Fleet"}
freeLicense := &fleet.LicenseInfo{Tier: "free"}
// First time running, we send statistics
stats, shouldSend, err := ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, license)
stats, shouldSend, err := ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, premiumLicense)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.NotEmpty(t, stats.AnonymousIdentifier)
assert.NotEmpty(t, stats.FleetVersion)
assert.Equal(t, stats.LicenseTier, "premium")
assert.Equal(t, stats.Organization, "Fleet")
assert.Equal(t, stats.NumHostsEnrolled, 1)
assert.Equal(t, stats.NumUsers, 2)
assert.Equal(t, stats.NumTeams, 1)
@@ -152,7 +154,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// If we try right away, it shouldn't ask to send
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, license)
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, premiumLicense)
require.NoError(t, err)
assert.False(t, shouldSend)
@@ -224,10 +226,12 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// Lower the frequency to trigger an "outdated" sent
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, license)
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, premiumLicense)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, stats.LicenseTier, "premium")
assert.Equal(t, stats.Organization, "Fleet")
assert.Equal(t, stats.NumHostsEnrolled, 5)
assert.Equal(t, stats.NumUsers, 2)
assert.Equal(t, stats.NumWeeklyActiveUsers, 0) // no active user since last stats were sent
@@ -255,10 +259,12 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
// 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(ctx, time.Millisecond, fleetConfig, license)
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, premiumLicense)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, stats.LicenseTier, "premium")
assert.Equal(t, stats.Organization, "Fleet")
assert.Equal(t, stats.NumHostsEnrolled, 5)
assert.Equal(t, stats.NumUsers, 2)
assert.Equal(t, stats.NumWeeklyActiveUsers, 1)
@@ -279,10 +285,21 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, license)
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, premiumLicense)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, stats.LicenseTier, "premium")
assert.Equal(t, stats.Organization, "Fleet")
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(ctx, time.Millisecond, fleetConfig, freeLicense)
require.NoError(t, err)
assert.True(t, shouldSend)
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
assert.Equal(t, stats.LicenseTier, "free")
assert.Equal(t, stats.Organization, "unknown")
}
+1
View File
@@ -9,6 +9,7 @@ type StatisticsPayload struct {
AnonymousIdentifier string `json:"anonymousIdentifier"`
FleetVersion string `json:"fleetVersion"`
LicenseTier string `json:"licenseTier"`
Organization string `json:"organization"`
NumHostsEnrolled int `json:"numHostsEnrolled"`
NumUsers int `json:"numUsers"`
NumTeams int `json:"numTeams"`
@@ -24,6 +24,7 @@ module.exports = {
hostsEnrolledByOperatingSystem: { type: 'json', defaultsTo: {} },
storedErrors: { type: 'json', defaultsTo: '[]' },
numHostsNotResponding: { type: 'number', defaultsTo: 0, description: 'The number of hosts per deployment that have not submitted results for distibuted queries. A host is counted as not responding if Fleet hasn\'t received a distributed write to requested distibuted queries for the host during the 2-hour interval since the host was last seen. Hosts that have not been seen for 7 days or more are not counted.', },
organization: { type: 'string', defaultsTo: 'unknown', description: 'For Fleet Premium deployments, the organization registered with the license.', },
},
+1
View File
@@ -28,6 +28,7 @@ module.exports = {
hostsEnrolledByOperatingSystem: { required: true, type: 'json' },
storedErrors: { required: true, type: 'json' },
numHostsNotResponding: { required: true, type: 'number', description: 'The number of hosts per deployment that have not submitted results for distibuted queries. A host is counted as not responding if Fleet hasn\'t received a distributed write to requested distibuted queries for the host during the 2-hour interval since the host was last seen. Hosts that have not been seen for 7 days or more are not counted.', },
organization: { required: true, type: 'string' },
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
+1 -1
View File
@@ -13,7 +13,7 @@
Cloud.setup({
/* eslint-disable */
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled","numWeeklyActiveUsers","hostsEnrolledByOperatingSystem","storedErrors"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":["emailAddress"]}}
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled","numWeeklyActiveUsers","hostsEnrolledByOperatingSystem","storedErrors","numHostsNotResponding","organization"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":["emailAddress"]}}
/* eslint-enable */
});