fleet/server/datastore/mysql/statistics.go
Sharon Katz 85f0638f4f
Add statistic to measure ABM pending hosts (#28226)
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
2025-04-15 11:30:07 -04:00

235 lines
9 KiB
Go

package mysql
import (
"context"
"database/sql"
"time"
"github.com/fleetdm/fleet/v4/server"
"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/fleetdm/fleet/v4/server/version"
"github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
)
type statistics struct {
fleet.UpdateCreateTimestamps
Identifier string `db:"anonymous_identifier"`
}
func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Duration, config config.FleetConfig) (fleet.StatisticsPayload, bool, error) {
lic, _ := license.FromContext(ctx)
computeStats := func(stats *fleet.StatisticsPayload, since time.Time) error {
enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount enrolled hosts by os")
}
numHostsABMPending, err := numHostsABMPendingDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts that are ABM pending")
}
amountUsers, err := tableRowsCount(ctx, ds.reader(ctx), "users")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount users")
}
amountSoftwaresVersions, err := tableRowsCount(ctx, ds.reader(ctx), "software")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software")
}
amountHostSoftwares, err := tableRowsCount(ctx, ds.reader(ctx), "host_software")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount host_software")
}
amountSoftwareTitles, err := tableRowsCount(ctx, ds.reader(ctx), "software_titles")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_titles")
}
amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.reader(ctx), "host_software_installed_paths")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount host_software_installed_paths")
}
amountSoftwareCpes, err := tableRowsCount(ctx, ds.reader(ctx), "software_cpe")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_cpe")
}
amountSoftwareCves, err := tableRowsCount(ctx, ds.reader(ctx), "software_cve")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_cve")
}
amountTeams, err := amountTeamsDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount teams")
}
amountPolicies, err := amountPoliciesDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount policies")
}
amountLabels, err := amountLabelsDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount labels")
}
appConfig, err := ds.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "statistics app config")
}
amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.reader(ctx), since)
if err != nil {
return ctxerr.Wrap(ctx, err, "amount active users")
}
amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.reader(ctx))
if err == sql.ErrNoRows {
level.Debug(ds.logger).Log("msg", "amount policy violation days", "err", err) //nolint:errcheck
} else if err != nil {
return ctxerr.Wrap(ctx, err, "amount policy violation days")
}
storedErrs, err := ctxerr.Aggregate(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "statistics error store")
}
amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.reader(ctx), ds.logger, config)
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts not responding")
}
amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts by orbit version")
}
amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.reader(ctx))
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")
}
numQueries, err := numSavedQueriesDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "number of saved queries in DB")
}
stats.NumHostsEnrolled = amountEnrolledHosts
stats.NumHostsABMPending = numHostsABMPending
stats.NumUsers = amountUsers
stats.NumSoftwareVersions = amountSoftwaresVersions
stats.NumHostSoftwares = amountHostSoftwares
stats.NumSoftwareTitles = amountSoftwareTitles
stats.NumHostSoftwareInstalledPaths = amountHostSoftwareInstalledPaths
stats.NumSoftwareCPEs = amountSoftwareCpes
stats.NumSoftwareCVEs = amountSoftwareCves
stats.NumTeams = amountTeams
stats.NumPolicies = amountPolicies
stats.NumLabels = amountLabels
stats.SoftwareInventoryEnabled = appConfig.Features.EnableSoftwareInventory
stats.VulnDetectionEnabled = config.Vulnerabilities.DatabasesPath != "" || appConfig.VulnerabilitySettings.DatabasesPath != ""
stats.SystemUsersEnabled = appConfig.Features.EnableHostUsers
stats.HostsStatusWebHookEnabled = appConfig.WebhookSettings.HostStatusWebhook.Enable
stats.MDMMacOsEnabled = appConfig.MDM.EnabledAndConfigured
stats.HostExpiryEnabled = appConfig.HostExpirySettings.HostExpiryEnabled
stats.MDMWindowsEnabled = appConfig.MDM.WindowsEnabledAndConfigured
stats.LiveQueryDisabled = appConfig.ServerSettings.LiveQueryDisabled
stats.NumWeeklyActiveUsers = amountWeeklyUsers
stats.NumWeeklyPolicyViolationDaysActual = amountPolicyViolationDaysActual
stats.NumWeeklyPolicyViolationDaysPossible = amountPolicyViolationDaysPossible
stats.HostsEnrolledByOperatingSystem = enrolledHostsByOS
stats.HostsEnrolledByOrbitVersion = amountHostsByOrbitVersion
stats.HostsEnrolledByOsqueryVersion = amountHostsByOsqueryVersion
stats.StoredErrors = storedErrs
stats.NumHostsNotResponding = amountHostsNotResponding
stats.Organization = "unknown"
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
stats.NumQueries = numQueries
return nil
}
dest := statistics{}
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
if err != nil {
if err == sql.ErrNoRows {
anonIdentifier, err := server.GenerateRandomText(64)
if err != nil {
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "generate random text")
}
_, err = ds.writer(ctx).ExecContext(ctx, `INSERT INTO statistics(anonymous_identifier) VALUES (?)`, anonIdentifier)
if err != nil {
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "insert statistics")
}
// compute active weekly users since now - frequency
stats := fleet.StatisticsPayload{
AnonymousIdentifier: anonIdentifier,
FleetVersion: version.Version().Version,
LicenseTier: fleet.TierFree,
}
if lic != nil {
stats.LicenseTier = lic.Tier
}
if err := computeStats(&stats, time.Now().Add(-frequency)); err != nil {
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "compute statistics")
}
return stats, true, nil
}
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "get statistics")
}
lastUpdated := dest.UpdatedAt
if dest.CreatedAt.After(dest.UpdatedAt) {
lastUpdated = dest.CreatedAt
}
if time.Now().Before(lastUpdated.Add(frequency)) {
return fleet.StatisticsPayload{}, false, nil
}
stats := fleet.StatisticsPayload{
AnonymousIdentifier: dest.Identifier,
FleetVersion: version.Version().Version,
LicenseTier: fleet.TierFree,
}
if lic != nil {
stats.LicenseTier = lic.Tier
}
if err := computeStats(&stats, lastUpdated); err != nil {
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "compute statistics")
}
return stats, true, nil
}
func (ds *Datastore) RecordStatisticsSent(ctx context.Context) error {
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE statistics SET updated_at = CURRENT_TIMESTAMP LIMIT 1`)
return ctxerr.Wrap(ctx, err, "update statistics")
}
func (ds *Datastore) CleanupStatistics(ctx context.Context) error {
// reset weekly count of policy violation days
if err := ds.InitializePolicyViolationDays(ctx); err != nil {
return err
}
return nil
}