fleet/server/service/testing_utils.go
Magnus Jensen a8c9e261d7
speed up macOS profile delivery for initial enrollments (#41960)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34433 

It speeds up the cron, meaning fleetd, bootstrap and now profiles should
be sent within 10 seconds of being known to fleet, compared to the
previous 1 minute.

It's heavily based on my last PR, so the structure and changes are close
to identical, with some small differences.
**I did not do the redis key part in this PR, as I think that should
come in it's own PR, to avoid overlooking logic bugs with that code, and
since this one is already quite sized since we're moving core pieces of
code around.**

# Checklist for submitter

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

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.


## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Faster macOS onboarding: device profiles are delivered and installed
as part of DEP enrollment, shortening initial setup.
* Improved profile handling: per-host profile preprocessing, secret
detection, and clearer failure marking.

* **Improvements**
  * Consolidated SCEP/NDES error messaging for clearer diagnostics.
  * Cron/work scheduling tuned to prioritize Apple MDM profile delivery.

* **Tests**
* Expanded MDM unit and integration tests, including
DeclarativeManagement handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-19 14:58:10 -05:00

1503 lines
44 KiB
Go

package service
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"sort"
"sync"
"testing"
"time"
"github.com/WatchBeam/clock"
ma "github.com/fleetdm/fleet/v4/ee/maintained-apps"
"github.com/fleetdm/fleet/v4/ee/server/scim"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
"github.com/fleetdm/fleet/v4/ee/server/service/condaccess"
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
"github.com/fleetdm/fleet/v4/ee/server/service/est"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
"github.com/fleetdm/fleet/v4/ee/server/service/scep"
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
"github.com/fleetdm/fleet/v4/server/authz"
"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/datastore/cached_mysql"
"github.com/fleetdm/fleet/v4/server/datastore/filesystem"
"github.com/fleetdm/fleet/v4/server/datastore/redis"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/dev_mode"
"github.com/fleetdm/fleet/v4/server/errorstore"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/logging"
"github.com/fleetdm/fleet/v4/server/mail"
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
fleet_mock "github.com/fleetdm/fleet/v4/server/mock"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-kit/kit/endpoint"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
"google.golang.org/api/androidmanagement/v1"
)
func newTestService(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) {
return newTestServiceWithConfig(t, ds, config.TestConfig(), rs, lq, opts...)
}
func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig config.FleetConfig, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) {
lic := &fleet.LicenseInfo{Tier: fleet.TierFree}
logger := slog.New(slog.DiscardHandler)
writer, err := logging.NewFilesystemLogWriter(t.Context(), fleetConfig.Filesystem.StatusLogFile, logger, fleetConfig.Filesystem.EnableLogRotation,
fleetConfig.Filesystem.EnableLogCompression, 500, 28, 3)
require.NoError(t, err)
osqlogger := &OsqueryLogger{Status: writer, Result: writer}
var (
failingPolicySet fleet.FailingPolicySet = NewMemFailingPolicySet()
enrollHostLimiter fleet.EnrollHostLimiter = nopEnrollHostLimiter{}
depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{}
mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }}
c clock.Clock = clock.C
scepConfigService = scep.NewSCEPConfigService(logger, nil)
digiCertService = digicert.NewService(digicert.WithLogger(logger))
estCAService = est.NewService(est.WithLogger(logger))
conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
mdmStorage fleet.MDMAppleStore
mdmPusher nanomdm_push.Pusher
ssoStore sso.SessionStore
profMatcher fleet.ProfileMatcher
softwareInstallStore fleet.SoftwareInstallerStore
bootstrapPackageStore fleet.MDMBootstrapPackageStore
softwareTitleIconStore fleet.SoftwareTitleIconStore
distributedLock fleet.Lock
keyValueStore fleet.KeyValueStore
androidService android.Service
)
if len(opts) > 0 {
if opts[0].Clock != nil {
c = opts[0].Clock
}
}
if len(opts) > 0 && opts[0].KeyValueStore != nil {
keyValueStore = opts[0].KeyValueStore
}
task := async.NewTask(ds, nil, c, nil)
if len(opts) > 0 {
if opts[0].Task != nil {
task = opts[0].Task
} else {
opts[0].Task = task
}
}
if len(opts) > 0 {
if opts[0].Logger != nil {
logger = opts[0].Logger
}
if opts[0].License != nil {
lic = opts[0].License
}
if opts[0].Pool != nil {
ssoStore = sso.NewSessionStore(opts[0].Pool)
profMatcher = apple_mdm.NewProfileMatcher(opts[0].Pool)
distributedLock = redis_lock.NewLock(opts[0].Pool)
keyValueStore = redis_key_value.New(opts[0].Pool)
}
if opts[0].ProfileMatcher != nil {
profMatcher = opts[0].ProfileMatcher
}
if opts[0].FailingPolicySet != nil {
failingPolicySet = opts[0].FailingPolicySet
}
if opts[0].EnrollHostLimiter != nil {
enrollHostLimiter = opts[0].EnrollHostLimiter
}
if opts[0].UseMailService {
mailer, err = mail.NewService(config.TestConfig())
require.NoError(t, err)
}
if opts[0].SoftwareInstallStore != nil {
softwareInstallStore = opts[0].SoftwareInstallStore
}
if opts[0].BootstrapPackageStore != nil {
bootstrapPackageStore = opts[0].BootstrapPackageStore
}
if opts[0].SoftwareTitleIconStore != nil {
softwareTitleIconStore = opts[0].SoftwareTitleIconStore
}
// allow to explicitly set MDM storage to nil
mdmStorage = opts[0].MDMStorage
if opts[0].DEPStorage != nil {
depStorage = opts[0].DEPStorage
}
// allow to explicitly set mdm pusher to nil
mdmPusher = opts[0].MDMPusher
}
ctx := license.NewContext(context.Background(), lic)
cronSchedulesService := fleet.NewCronSchedules()
var eh *errorstore.Handler
if len(opts) > 0 {
if opts[0].Pool != nil {
eh = errorstore.NewHandler(ctx, opts[0].Pool, logger, time.Minute*10)
ctx = ctxerr.NewContext(ctx, eh)
}
if opts[0].StartCronSchedules != nil {
for _, fn := range opts[0].StartCronSchedules {
err = cronSchedulesService.StartCronSchedule(fn(ctx, ds))
require.NoError(t, err)
}
}
}
if len(opts) > 0 && opts[0].SCEPConfigService != nil {
scepConfigService = opts[0].SCEPConfigService
}
if len(opts) > 0 && opts[0].DigiCertService != nil {
digiCertService = opts[0].DigiCertService
}
if len(opts) > 0 && opts[0].ConditionalAccessMicrosoftProxy != nil {
conditionalAccessMicrosoftProxy = opts[0].ConditionalAccessMicrosoftProxy
fleetConfig.MicrosoftCompliancePartner.ProxyAPIKey = "insecure" // setting this so the feature is "enabled".
}
if len(opts) > 0 && opts[0].androidModule != nil {
androidService = opts[0].androidModule
}
var wstepManager microsoft_mdm.CertManager
if fleetConfig.MDM.WindowsWSTEPIdentityCert != "" && fleetConfig.MDM.WindowsWSTEPIdentityKey != "" {
rawCert, err := os.ReadFile(fleetConfig.MDM.WindowsWSTEPIdentityCert)
require.NoError(t, err)
rawKey, err := os.ReadFile(fleetConfig.MDM.WindowsWSTEPIdentityKey)
require.NoError(t, err)
wstepManager, err = microsoft_mdm.NewCertManager(ds, rawCert, rawKey)
require.NoError(t, err)
}
svc, err := NewService(
ctx,
ds,
task,
rs,
logger,
osqlogger,
fleetConfig,
mailer,
c,
ssoStore,
lq,
ds,
failingPolicySet,
&fleet.NoOpGeoIP{},
enrollHostLimiter,
depStorage,
mdmStorage,
mdmPusher,
cronSchedulesService,
wstepManager,
scepConfigService,
digiCertService,
conditionalAccessMicrosoftProxy,
keyValueStore,
androidService,
)
if err != nil {
panic(err)
}
if lic.IsPremium() {
if softwareInstallStore == nil {
// default to file-based
dir := t.TempDir()
store, err := filesystem.NewSoftwareInstallerStore(dir)
if err != nil {
panic(err)
}
softwareInstallStore = store
}
var androidModule android.Service
if len(opts) > 0 {
androidModule = opts[0].androidModule
}
svc, err = eeservice.NewService(
svc,
ds,
logger,
fleetConfig,
mailer,
c,
depStorage,
apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher),
ssoStore,
profMatcher,
softwareInstallStore,
bootstrapPackageStore,
softwareTitleIconStore,
distributedLock,
keyValueStore,
scepConfigService,
digiCertService,
androidModule,
estCAService,
)
if err != nil {
panic(err)
}
}
// Set up mock activity service for unit tests. When DBConns is provided,
// RunServerForTestsWithServiceWithDS will overwrite this with the real bounded context.
activityMock := &fleet_mock.MockActivityService{
NewActivityFunc: func(_ context.Context, _ *activity_api.User, _ activity_api.ActivityDetails) error {
return nil
},
}
svc.SetActivityService(activityMock)
if len(opts) > 0 {
opts[0].ActivityMock = activityMock
}
return svc, ctx
}
func newTestServiceWithClock(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, c clock.Clock) (fleet.Service, context.Context) {
testConfig := config.TestConfig()
return newTestServiceWithConfig(t, ds, testConfig, rs, lq, &TestServerOpts{
Clock: c,
})
}
func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
users := make(map[string]fleet.User)
// Map iteration is random so we sort and iterate using the testUsers keys.
var keys []string
for key := range testUsers {
keys = append(keys, key)
}
sort.Strings(keys)
userID := uint(1)
for _, key := range keys {
u := testUsers[key]
user := &fleet.User{
ID: userID, // We need to set this in case ds is a mocked Datastore.
Name: "Test Name " + u.Email,
Email: u.Email,
GlobalRole: u.GlobalRole,
}
err := user.SetPassword(u.PlaintextPassword, 10, 10)
require.Nil(t, err)
user, err = ds.NewUser(context.Background(), user)
require.Nil(t, err)
users[user.Email] = *user
userID++
}
return users
}
const (
TestAdminUserEmail = "admin1@example.com"
TestMaintainerUserEmail = "user1@example.com"
TestObserverUserEmail = "user2@example.com"
)
var testUsers = map[string]struct {
Email string
PlaintextPassword string
GlobalRole *string
}{
"admin1": {
PlaintextPassword: test.GoodPassword,
Email: TestAdminUserEmail,
GlobalRole: ptr.String(fleet.RoleAdmin),
},
"user1": {
PlaintextPassword: test.GoodPassword,
Email: TestMaintainerUserEmail,
GlobalRole: ptr.String(fleet.RoleMaintainer),
},
"user2": {
PlaintextPassword: test.GoodPassword,
Email: TestObserverUserEmail,
GlobalRole: ptr.String(fleet.RoleObserver),
},
}
func createEnrollSecrets(t *testing.T, count int) []*fleet.EnrollSecret {
secrets := make([]*fleet.EnrollSecret, count)
for i := 0; i < count; i++ {
secrets[i] = &fleet.EnrollSecret{Secret: fmt.Sprintf("testSecret%d", i)}
}
return secrets
}
type mockMailService struct {
SendEmailFn func(e fleet.Email) error
Invoked bool
}
func (svc *mockMailService) SendEmail(ctx context.Context, e fleet.Email) error {
svc.Invoked = true
return svc.SendEmailFn(e)
}
func (svc *mockMailService) CanSendEmail(smtpSettings fleet.SMTPSettings) bool {
return smtpSettings.SMTPConfigured
}
type TestNewScheduleFunc func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc
// HostIdentity combines host identity-related test options
type HostIdentity struct {
SCEPStorage scep_depot.Depot
RequireHTTPMessageSignature bool
}
// ConditionalAccess combines conditional access-related test options
type ConditionalAccess struct {
SCEPStorage scep_depot.Depot
}
type TestServerOpts struct {
Logger *slog.Logger
License *fleet.LicenseInfo
SkipCreateTestUsers bool
Rs fleet.QueryResultStore
Lq fleet.LiveQueryStore
Pool fleet.RedisPool
FailingPolicySet fleet.FailingPolicySet
Clock clock.Clock
Task *async.Task
EnrollHostLimiter fleet.EnrollHostLimiter
Is fleet.InstallerStore
FleetConfig *config.FleetConfig
MDMStorage fleet.MDMAppleStore
DEPStorage nanodep_storage.AllDEPStorage
SCEPStorage scep_depot.Depot
MDMPusher nanomdm_push.Pusher
HTTPServerConfig *http.Server
StartCronSchedules []TestNewScheduleFunc
UseMailService bool
APNSTopic string
ProfileMatcher fleet.ProfileMatcher
EnableCachedDS bool
NoCacheDatastore bool
SoftwareInstallStore fleet.SoftwareInstallerStore
BootstrapPackageStore fleet.MDMBootstrapPackageStore
SoftwareTitleIconStore fleet.SoftwareTitleIconStore
KeyValueStore fleet.KeyValueStore
EnableSCEPProxy bool
WithDEPWebview bool
FeatureRoutes []endpointer.HandlerRoutesFunc
SCEPConfigService fleet.SCEPConfigService
DigiCertService fleet.DigiCertService
EnableSCIM bool
ConditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
HostIdentity *HostIdentity
androidMockClient *android_mock.Client
androidModule android.Service
ConditionalAccess *ConditionalAccess
DBConns *common_mysql.DBConnections
// ActivityMock is populated automatically by newTestServiceWithConfig.
// After setup, tests can use it to intercept or assert on activity creation.
ActivityMock *fleet_mock.MockActivityService
}
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
if len(opts) > 0 && opts[0].EnableCachedDS {
ds = cached_mysql.New(ds)
}
cfg := config.TestConfig()
if len(opts) > 0 && opts[0].FleetConfig != nil {
cfg = *opts[0].FleetConfig
}
svc, ctx := NewTestService(t, ds, cfg, opts...)
return RunServerForTestsWithServiceWithDS(t, ctx, ds, svc, opts...)
}
func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fleet.Datastore, svc fleet.Service,
opts ...*TestServerOpts,
) (map[string]fleet.User, *httptest.Server) {
var cfg config.FleetConfig
if len(opts) > 0 && opts[0].FleetConfig != nil {
cfg = *opts[0].FleetConfig
} else {
cfg = config.TestConfig()
}
users := map[string]fleet.User{}
if len(opts) == 0 || (len(opts) > 0 && !opts[0].SkipCreateTestUsers) {
users = createTestUsers(t, ds)
}
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
if len(opts) > 0 && opts[0].Logger != nil {
logger = opts[0].Logger
}
if len(opts) > 0 {
opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, android_service.GetRoutes(svc, opts[0].androidModule))
}
// Add activity routes if DBConns is provided
if len(opts) > 0 && opts[0].DBConns != nil {
legacyAuthorizer, err := authz.NewAuthorizer()
require.NoError(t, err)
activityAuthorizer := authz.NewAuthorizerAdapter(legacyAuthorizer)
activityACLAdapter := activityacl.NewFleetServiceAdapter(svc)
activitySvc, activityRoutesFn := activity_bootstrap.New(
opts[0].DBConns,
activityAuthorizer,
activityACLAdapter,
logger,
)
svc.SetActivityService(activitySvc)
activityAuthMiddleware := func(next endpoint.Endpoint) endpoint.Endpoint {
return auth.AuthenticatedUser(svc, next)
}
opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, activityRoutesFn(activityAuthMiddleware))
}
var mdmPusher nanomdm_push.Pusher
if len(opts) > 0 && opts[0].MDMPusher != nil {
mdmPusher = opts[0].MDMPusher
}
rootMux := http.NewServeMux()
memLimitStore, _ := memstore.New(0)
var limitStore throttled.GCRAStore = memLimitStore
var redisPool fleet.RedisPool
if len(opts) > 0 && opts[0].Pool != nil {
redisPool = opts[0].Pool
limitStore = &redis.ThrottledStore{
Pool: opts[0].Pool,
KeyPrefix: "ratelimit::",
}
} else {
redisPool = redistest.SetupRedis(t, t.Name(), false, false, false) // We are good to initalize a redis pool here as it is only called by integration tests
}
if len(opts) > 0 {
mdmStorage := opts[0].MDMStorage
scepStorage := opts[0].SCEPStorage
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher)
if mdmStorage != nil && scepStorage != nil {
vppInstaller := svc.(fleet.AppleMDMVPPInstaller)
checkInAndCommand := NewMDMAppleCheckinAndCommandService(ds, commander, vppInstaller, opts[0].License.IsPremium(), logger, redis_key_value.New(redisPool), svc.NewActivity)
checkInAndCommand.RegisterResultsHandler("InstalledApplicationList", NewInstalledApplicationListResultsHandler(ds, commander, logger, cfg.Server.VPPVerifyTimeout, cfg.Server.VPPVerifyRequestDelay, svc.NewActivity))
checkInAndCommand.RegisterResultsHandler(fleet.DeviceLocationCmdName, NewDeviceLocationResultsHandler(ds, commander, logger))
checkInAndCommand.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity))
err := RegisterAppleMDMProtocolServices(
rootMux,
cfg.MDM,
mdmStorage,
scepStorage,
logger,
checkInAndCommand,
&MDMAppleDDMService{
ds: ds,
logger: logger,
},
commander,
"https://test-url.com",
cfg,
)
require.NoError(t, err)
}
if opts[0].EnableSCEPProxy {
var timeout *time.Duration
if opts[0].SCEPConfigService != nil {
scepConfig, ok := opts[0].SCEPConfigService.(*scep.SCEPConfigService)
if ok {
// In tests, we share the same Timeout pointer between SCEPConfigService and SCEPProxy
timeout = scepConfig.Timeout
}
}
err := RegisterSCEPProxy(
rootMux,
ds,
logger,
timeout,
&cfg,
)
require.NoError(t, err)
}
}
if len(opts) > 0 && opts[0].WithDEPWebview {
frontendHandler := WithMDMEnrollmentMiddleware(svc, logger, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do nothing and return 200
w.WriteHeader(http.StatusOK)
}))
rootMux.Handle("/", frontendHandler)
}
var featureRoutes []endpointer.HandlerRoutesFunc
if len(opts) > 0 && len(opts[0].FeatureRoutes) > 0 {
featureRoutes = opts[0].FeatureRoutes
}
var extra []ExtraHandlerOption
extra = append(extra, WithLoginRateLimit(throttled.PerMin(1000)))
if len(opts) > 0 && opts[0].HostIdentity != nil {
require.NoError(t, hostidentity.RegisterSCEP(rootMux, opts[0].HostIdentity.SCEPStorage, ds, logger, &cfg))
var httpSigVerifier func(http.Handler) http.Handler
httpSigVerifier, err := httpsig.Middleware(ds, opts[0].HostIdentity.RequireHTTPMessageSignature,
logger.With("component", "http-sig-verifier"))
require.NoError(t, err)
extra = append(extra, WithHTTPSigVerifier(httpSigVerifier))
}
if len(opts) > 0 && opts[0].ConditionalAccess != nil {
require.NoError(t, condaccess.RegisterSCEP(ctx, rootMux, opts[0].ConditionalAccess.SCEPStorage, ds, logger, &cfg))
require.NoError(t, condaccess.RegisterIdP(rootMux, ds, logger, &cfg, limitStore))
}
var carveStore fleet.CarveStore = ds // In tests, we use MySQL as storage for carves.
apiHandler := MakeHandler(svc, cfg, logger, limitStore, redisPool, carveStore, featureRoutes, extra...)
rootMux.Handle("/api/", apiHandler)
var errHandler *errorstore.Handler
ctxErrHandler := ctxerr.FromContext(ctx)
if ctxErrHandler != nil {
errHandler = ctxErrHandler.(*errorstore.Handler)
}
debugHandler := MakeDebugHandler(svc, cfg, logger, errHandler, ds)
rootMux.Handle("/debug/", debugHandler)
rootMux.Handle("/enroll", ServeEndUserEnrollOTA(svc, "", ds, logger, false))
if len(opts) > 0 && opts[0].EnableSCIM {
require.NoError(t, scim.RegisterSCIM(rootMux, ds, svc, logger, &cfg))
rootMux.Handle("/api/v1/fleet/scim/details", apiHandler)
rootMux.Handle("/api/latest/fleet/scim/details", apiHandler)
}
server := httptest.NewUnstartedServer(rootMux)
server.Config = cfg.Server.DefaultHTTPServer(ctx, rootMux)
// WriteTimeout is set for security purposes.
// If we don't set it, (bugy or malignant) clients making long running
// requests could DDOS Fleet.
require.NotZero(t, server.Config.WriteTimeout)
if len(opts) > 0 && opts[0].HTTPServerConfig != nil {
server.Config = opts[0].HTTPServerConfig
// make sure we use the application handler we just created
server.Config.Handler = rootMux
}
server.Start()
t.Cleanup(func() {
server.Close()
})
return users, server
}
func NewTestService(t *testing.T, ds fleet.Datastore, cfg config.FleetConfig, opts ...*TestServerOpts) (fleet.Service, context.Context) {
var rs fleet.QueryResultStore
if len(opts) > 0 && opts[0].Rs != nil {
rs = opts[0].Rs
}
var lq fleet.LiveQueryStore
if len(opts) > 0 && opts[0].Lq != nil {
lq = opts[0].Lq
}
return newTestServiceWithConfig(t, ds, cfg, rs, lq, opts...)
}
func testSESPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Email = config.EmailConfig{EmailBackend: "ses"}
c.SES = config.SESConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
SourceArn: "qux",
}
return c
}
func testKinesisPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "kinesis"
c.Osquery.StatusLogPlugin = "kinesis"
c.Activity.AuditLogPlugin = "kinesis"
c.Kinesis = config.KinesisConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
StatusStream: "test-status-stream",
ResultStream: "test-result-stream",
AuditStream: "test-audit-stream",
}
return c
}
func testFirehosePluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "firehose"
c.Osquery.StatusLogPlugin = "firehose"
c.Activity.AuditLogPlugin = "firehose"
c.Firehose = config.FirehoseConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
StatusStream: "test-status-firehose",
ResultStream: "test-result-firehose",
AuditStream: "test-audit-firehose",
}
return c
}
func testLambdaPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "lambda"
c.Osquery.StatusLogPlugin = "lambda"
c.Activity.AuditLogPlugin = "lambda"
c.Lambda = config.LambdaConfig{
Region: "us-east-1",
AccessKeyID: "foo",
SecretAccessKey: "bar",
StsAssumeRoleArn: "baz",
ResultFunction: "result-func",
StatusFunction: "status-func",
AuditFunction: "audit-func",
}
return c
}
func testPubSubPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "pubsub"
c.Osquery.StatusLogPlugin = "pubsub"
c.Activity.AuditLogPlugin = "pubsub"
c.PubSub = config.PubSubConfig{
Project: "test",
StatusTopic: "status-topic",
ResultTopic: "result-topic",
AuditTopic: "audit-topic",
AddAttributes: false,
}
return c
}
func testStdoutPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery.ResultLogPlugin = "stdout"
c.Osquery.StatusLogPlugin = "stdout"
c.Activity.AuditLogPlugin = "stdout"
return c
}
func testUnrecognizedPluginConfig() config.FleetConfig {
c := config.TestConfig()
c.Osquery = config.OsqueryConfig{
ResultLogPlugin: "bar",
StatusLogPlugin: "bar",
}
c.Activity.AuditLogPlugin = "bar"
return c
}
func assertBodyContains(t *testing.T, resp *http.Response, expected string) {
bodyBytes, err := io.ReadAll(resp.Body)
require.Nil(t, err)
bodyString := string(bodyBytes)
assert.Contains(t, bodyString, expected)
}
func getJSON(r *http.Response, target interface{}) error {
return json.NewDecoder(r.Body).Decode(target)
}
func assertErrorCodeAndMessage(t *testing.T, resp *http.Response, code int, message string) {
err := &fleet.Error{}
require.Nil(t, getJSON(resp, err))
assert.Equal(t, code, err.Code)
assert.Equal(t, message, err.Message)
}
type memFailingPolicySet struct {
mMu sync.RWMutex
m map[uint][]fleet.PolicySetHost
}
var _ fleet.FailingPolicySet = (*memFailingPolicySet)(nil)
func NewMemFailingPolicySet() *memFailingPolicySet {
return &memFailingPolicySet{
m: make(map[uint][]fleet.PolicySetHost),
}
}
// AddFailingPoliciesForHost adds the given host to the policy sets.
func (m *memFailingPolicySet) AddHost(policyID uint, host fleet.PolicySetHost) error {
m.mMu.Lock()
defer m.mMu.Unlock()
m.m[policyID] = append(m.m[policyID], host)
return nil
}
// ListHosts returns the list of hosts present in the policy set.
func (m *memFailingPolicySet) ListHosts(policyID uint) ([]fleet.PolicySetHost, error) {
m.mMu.RLock()
defer m.mMu.RUnlock()
hosts := make([]fleet.PolicySetHost, len(m.m[policyID]))
copy(hosts, m.m[policyID])
return hosts, nil
}
// RemoveHosts removes the hosts from the policy set.
func (m *memFailingPolicySet) RemoveHosts(policyID uint, hosts []fleet.PolicySetHost) error {
m.mMu.Lock()
defer m.mMu.Unlock()
if _, ok := m.m[policyID]; !ok {
return nil
}
hostsSet := make(map[uint]struct{})
for _, host := range hosts {
hostsSet[host.ID] = struct{}{}
}
n := 0
for _, host := range m.m[policyID] {
if _, ok := hostsSet[host.ID]; !ok {
m.m[policyID][n] = host
n++
}
}
m.m[policyID] = m.m[policyID][:n]
return nil
}
// RemoveSet removes a policy set.
func (m *memFailingPolicySet) RemoveSet(policyID uint) error {
m.mMu.Lock()
defer m.mMu.Unlock()
delete(m.m, policyID)
return nil
}
// ListSets lists all the policy sets.
func (m *memFailingPolicySet) ListSets() ([]uint, error) {
m.mMu.RLock()
defer m.mMu.RUnlock()
var policyIDs []uint
for policyID := range m.m {
policyIDs = append(policyIDs, policyID)
}
return policyIDs, nil
}
type nopEnrollHostLimiter struct{}
func (nopEnrollHostLimiter) CanEnrollNewHost(ctx context.Context) (bool, error) {
return true, nil
}
func (nopEnrollHostLimiter) SyncEnrolledHostIDs(ctx context.Context) error {
return nil
}
func newMockAPNSPushProviderFactory() (*mock.APNSPushProviderFactory, *mock.APNSPushProvider) {
provider := &mock.APNSPushProvider{}
provider.PushFunc = mockSuccessfulPush
factory := &mock.APNSPushProviderFactory{}
factory.NewPushProviderFunc = func(*tls.Certificate) (push.PushProvider, error) {
return provider, nil
}
return factory, provider
}
func mockSuccessfulPush(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
res := make(map[string]*push.Response, len(pushes))
for _, p := range pushes {
res[p.Token.String()] = &push.Response{
Id: uuid.New().String(),
Err: nil,
}
}
return res, nil
}
func mdmConfigurationRequiredEndpoints() []struct {
method, path string
deviceAuthenticated bool
premiumOnly bool
} {
return []struct {
method, path string
deviceAuthenticated bool
premiumOnly bool
}{
{"POST", "/api/latest/fleet/mdm/apple/enqueue", false, false},
{"GET", "/api/latest/fleet/mdm/apple/commandresults", false, false},
{"GET", "/api/latest/fleet/mdm/apple/installers/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/installers/1", false, false},
{"GET", "/api/latest/fleet/mdm/apple/installers", false, false},
{"GET", "/api/latest/fleet/mdm/apple/devices", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/profiles/1", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles/summary", false, false},
{"PATCH", "/api/latest/fleet/mdm/hosts/1/unenroll", false, false},
{"DELETE", "/api/latest/fleet/hosts/1/mdm", false, false},
{"GET", "/api/latest/fleet/mdm/hosts/1/profiles", false, true},
{"GET", "/api/latest/fleet/hosts/1/configuration_profiles", false, true},
{"POST", "/api/latest/fleet/mdm/hosts/1/lock", false, false},
{"POST", "/api/latest/fleet/mdm/hosts/1/wipe", false, false},
{"PATCH", "/api/latest/fleet/mdm/apple/settings", false, false},
{"GET", "/api/latest/fleet/mdm/apple", false, false},
{"GET", "/api/latest/fleet/apns", false, false},
{"GET", apple_mdm.EnrollPath + "?token=test", false, false},
{"GET", apple_mdm.InstallerPath + "?token=test", false, false},
{"GET", "/api/latest/fleet/mdm/setup/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, true},
{"GET", "/api/latest/fleet/setup_experience/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, true},
{"DELETE", "/api/latest/fleet/mdm/setup/eula/token", false, true},
{"DELETE", "/api/latest/fleet/setup_experience/eula/token", false, true},
{"GET", "/api/latest/fleet/mdm/setup/eula/metadata", false, true},
{"GET", "/api/latest/fleet/setup_experience/eula/metadata", false, true},
{"GET", "/api/latest/fleet/mdm/apple/setup/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/setup/eula/token", false, false},
{"GET", "/api/latest/fleet/mdm/apple/setup/eula/metadata", false, false},
{"GET", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false},
{"GET", "/api/latest/fleet/enrollment_profiles/automatic", false, false},
{"POST", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false},
{"POST", "/api/latest/fleet/enrollment_profiles/automatic", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false},
{"DELETE", "/api/latest/fleet/enrollment_profiles/automatic", false, false},
{"POST", "/api/latest/fleet/device/%s/migrate_mdm", true, true},
{"POST", "/api/latest/fleet/mdm/apple/profiles/preassign", false, true},
{"POST", "/api/latest/fleet/mdm/apple/profiles/match", false, true},
{"POST", "/api/latest/fleet/mdm/commands/run", false, false},
{"POST", "/api/latest/fleet/commands/run", false, false},
{"GET", "/api/latest/fleet/mdm/commandresults", false, false},
{"GET", "/api/latest/fleet/commands/results", false, false},
{"GET", "/api/latest/fleet/mdm/commands", false, false},
{"GET", "/api/latest/fleet/commands", false, false},
{"POST", "/api/fleet/orbit/disk_encryption_key", false, false},
{"GET", "/api/latest/fleet/mdm/profiles/1", false, false},
{"GET", "/api/latest/fleet/configuration_profiles/1", false, false},
// TODO: those endpoints accept multipart/form data that gets
// parsed before the MDM check, we need to refactor this
// function to return more information to the caller, or find a
// better way to test these endpoints.
// {"POST", "/api/latest/fleet/mdm/profiles", false, false},
// {"POST", "/api/latest/fleet/configuration_profiles", false, false},
// {"POST", "/api/latest/fleet/mdm/setup/eula"},
// {"POST", "/api/latest/fleet/setup_experience/eula"},
// {"POST", "/api/latest/fleet/mdm/bootstrap", false, true},
// {"POST", "/api/latest/fleet/bootstrap", false, true},
{"GET", "/api/latest/fleet/mdm/profiles", false, false},
{"GET", "/api/latest/fleet/configuration_profiles", false, false},
{"GET", "/api/latest/fleet/mdm/manual_enrollment_profile", false, true},
{"GET", "/api/latest/fleet/enrollment_profiles/manual", false, true},
{"GET", "/api/latest/fleet/mdm/bootstrap/1/metadata", false, true},
{"GET", "/api/latest/fleet/bootstrap/1/metadata", false, true},
{"DELETE", "/api/latest/fleet/mdm/bootstrap/1", false, true},
{"DELETE", "/api/latest/fleet/bootstrap/1", false, true},
{"GET", "/api/latest/fleet/mdm/bootstrap?token=1", false, true},
{"GET", "/api/latest/fleet/bootstrap?token=1", false, true},
{"GET", "/api/latest/fleet/mdm/bootstrap/summary", false, true},
{"GET", "/api/latest/fleet/mdm/apple/bootstrap/summary", false, true},
{"GET", "/api/latest/fleet/bootstrap/summary", false, true},
{"PATCH", "/api/latest/fleet/mdm/apple/setup", false, true},
{"PATCH", "/api/latest/fleet/setup_experience", false, true},
{"POST", "/api/fleet/orbit/setup_experience/status", false, true},
{"POST", "/api/latest/fleet/software/web_apps", false, true},
}
}
func windowsMDMConfigurationRequiredEndpoints() []string {
return []string{
"/api/fleet/orbit/disk_encryption_key",
}
}
func androidMDMConfigurationRequiredEndpoints() []string {
return []string{
"/api/latest/fleet/software/web_apps",
}
}
// getURLSchemas returns a list of all valid URI schemas
func getURISchemas() []string {
return []string{
"aaa",
"aaas",
"about",
"acap",
"acct",
"acd",
"acr",
"adiumxtra",
"adt",
"afp",
"afs",
"aim",
"amss",
"android",
"appdata",
"apt",
"ar",
"ark",
"at",
"attachment",
"aw",
"barion",
"bb",
"beshare",
"bitcoin",
"bitcoincash",
"blob",
"bolo",
"browserext",
"cabal",
"calculator",
"callto",
"cap",
"cast",
"casts",
"chrome",
"chrome-extension",
"cid",
"coap",
"coap+tcp",
"coap+ws",
"coaps",
"coaps+tcp",
"coaps+ws",
"com-eventbrite-attendee",
"content",
"content-type",
"crid",
"cstr",
"cvs",
"dab",
"dat",
"data",
"dav",
"dhttp",
"diaspora",
"dict",
"did",
"dis",
"dlna-playcontainer",
"dlna-playsingle",
"dns",
"dntp",
"doi",
"dpp",
"drm",
"drop",
"dtmi",
"dtn",
"dvb",
"dvx",
"dweb",
"ed2k",
"eid",
"elsi",
"embedded",
"ens",
"ethereum",
"example",
"facetime",
"fax",
"feed",
"feedready",
"fido",
"file",
"filesystem",
"finger",
"first-run-pen-experience",
"fish",
"fm",
"ftp",
"fuchsia-pkg",
"geo",
"gg",
"git",
"gitoid",
"gizmoproject",
"go",
"gopher",
"graph",
"grd",
"gtalk",
"h323",
"ham",
"hcap",
"hcp",
"http",
"https",
"hxxp",
"hxxps",
"hydrazone",
"hyper",
"iax",
"icap",
"icon",
"im",
"imap",
"info",
"iotdisco",
"ipfs",
"ipn",
"ipns",
"ipp",
"ipps",
"irc",
"irc6",
"ircs",
"iris",
"iris.beep",
"iris.lwz",
"iris.xpc",
"iris.xpcs",
"isostore",
"itms",
"jabber",
"jar",
"jms",
"keyparc",
"lastfm",
"lbry",
"ldap",
"ldaps",
"leaptofrogans",
"lorawan",
"lpa",
"lvlt",
"magnet",
"mailserver",
"mailto",
"maps",
"market",
"matrix",
"message",
"microsoft.windows.camera",
"microsoft.windows.camera.multipicker",
"microsoft.windows.camera.picker",
"mid",
"mms",
"modem",
"mongodb",
"moz",
"ms-access",
"ms-appinstaller",
"ms-browser-extension",
"ms-calculator",
"ms-drive-to",
"ms-enrollment",
"ms-excel",
"ms-eyecontrolspeech",
"ms-gamebarservices",
"ms-gamingoverlay",
"ms-getoffice",
"ms-help",
"ms-infopath",
"ms-inputapp",
"ms-launchremotedesktop",
"ms-lockscreencomponent-config",
"ms-media-stream-id",
"ms-meetnow",
"ms-mixedrealitycapture",
"ms-mobileplans",
"ms-newsandinterests",
"ms-officeapp",
"ms-people",
"ms-project",
"ms-powerpoint",
"ms-publisher",
"ms-remotedesktop",
"ms-remotedesktop-launch",
"ms-restoretabcompanion",
"ms-screenclip",
"ms-screensketch",
"ms-search",
"ms-search-repair",
"ms-secondary-screen-controller",
"ms-secondary-screen-setup",
"ms-settings",
"ms-settings-airplanemode",
"ms-settings-bluetooth",
"ms-settings-camera",
"ms-settings-cellular",
"ms-settings-cloudstorage",
"ms-settings-connectabledevices",
"ms-settings-displays-topology",
"ms-settings-emailandaccounts",
"ms-settings-language",
"ms-settings-location",
"ms-settings-lock",
"ms-settings-nfctransactions",
"ms-settings-notifications",
"ms-settings-power",
"ms-settings-privacy",
"ms-settings-proximity",
"ms-settings-screenrotation",
"ms-settings-wifi",
"ms-settings-workplace",
"ms-spd",
"ms-stickers",
"ms-sttoverlay",
"ms-transit-to",
"ms-useractivityset",
"ms-virtualtouchpad",
"ms-visio",
"ms-walk-to",
"ms-whiteboard",
"ms-whiteboard-cmd",
"ms-word",
"msnim",
"msrp",
"msrps",
"mss",
"mt",
"mtqp",
"mumble",
"mupdate",
"mvn",
"news",
"nfs",
"ni",
"nih",
"nntp",
"notes",
"num",
"ocf",
"oid",
"onenote",
"onenote-cmd",
"opaquelocktoken",
"openpgp4fpr",
"otpauth",
"p1",
"pack",
"palm",
"paparazzi",
"payment",
"payto",
"pkcs11",
"platform",
"pop",
"pres",
"prospero",
"proxy",
"pwid",
"psyc",
"pttp",
"qb",
"query",
"quic-transport",
"redis",
"rediss",
"reload",
"res",
"resource",
"rmi",
"rsync",
"rtmfp",
"rtmp",
"rtsp",
"rtsps",
"rtspu",
"sarif",
"secondlife",
"secret-token",
"service",
"session",
"sftp",
"sgn",
"shc",
"shttp",
"sieve",
"simpleledger",
"simplex",
"sip",
"sips",
"skype",
"smb",
"smp",
"sms",
"smtp",
"snews",
"snmp",
"soap.beep",
"soap.beeps",
"soldat",
"spiffe",
"spotify",
"ssb",
"ssh",
"starknet",
"steam",
"stun",
"stuns",
"submit",
"svn",
"swh",
"swid",
"swidpath",
"tag",
"taler",
"teamspeak",
"tel",
"teliaeid",
"telnet",
"tftp",
"things",
"thismessage",
"tip",
"tn3270",
"tool",
"turn",
"turns",
"tv",
"udp",
"unreal",
"upt",
"urn",
"ut2004",
"uuid-in-package",
"v-event",
"vemmi",
"ventrilo",
"ves",
"videotex",
"vnc",
"view-source",
"vscode",
"vscode-insiders",
"vsls",
"w3",
"wais",
"web3",
"wcr",
"webcal",
"web+ap",
"wifi",
"wpid",
"ws",
"wss",
"wtai",
"wyciwyg",
"xcon",
"xcon-userid",
"xfire",
"xmlrpc.beep",
"xmlrpc.beeps",
"xmpp",
"xri",
"ymsgr",
"z39.50",
"z39.50r",
"z39.50s",
}
}
func createAndroidDeviceID(name string) string {
return "enterprises/mock-enterprise-id/devices/" + name
}
func statusReportMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubStatusReport, deviceInfo, enterpriseSpecificID, "")
}
func enrollmentMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubEnrollment, deviceInfo, enterpriseSpecificID, "")
}
func statusReportMessageWithSerialNumber(t *testing.T, deviceInfo androidmanagement.Device, serialNumber string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubStatusReport, deviceInfo, "", serialNumber)
}
func enrollmentMessageWithSerialNumber(t *testing.T, deviceInfo androidmanagement.Device, serialNumber string) *android.PubSubMessage {
return messageWithAndroidIdentifiers(t, android.PubSubEnrollment, deviceInfo, "", serialNumber)
}
func messageWithAndroidIdentifiers(t *testing.T, notificationType android.NotificationType, deviceInfo androidmanagement.Device, enterpriseSpecificID, serialNumber string) *android.PubSubMessage {
if serialNumber == "" && enterpriseSpecificID == "" || serialNumber != "" && enterpriseSpecificID != "" {
t.Fatalf("exactly one of serialNumber or enterpriseSpecificID must be provided")
}
deviceInfo.HardwareInfo = &androidmanagement.HardwareInfo{
Brand: "TestBrand",
Model: "TestModel",
Hardware: "test-hardware",
}
if enterpriseSpecificID != "" {
deviceInfo.Ownership = android_service.DeviceOwnershipPersonallyOwned
deviceInfo.HardwareInfo.EnterpriseSpecificId = enterpriseSpecificID
deviceInfo.HardwareInfo.SerialNumber = enterpriseSpecificID
}
if serialNumber != "" {
deviceInfo.Ownership = android_service.DeviceOwnershipCompanyOwned
deviceInfo.HardwareInfo.SerialNumber = serialNumber
}
deviceInfo.SoftwareInfo = &androidmanagement.SoftwareInfo{
AndroidBuildNumber: "test-build",
AndroidVersion: "1",
}
deviceInfo.MemoryInfo = &androidmanagement.MemoryInfo{
TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition
}
deviceInfo.MemoryEvents = []*androidmanagement.MemoryEvent{
{
EventType: "EXTERNAL_STORAGE_DETECTED",
ByteCount: int64(64 * 1024 * 1024 * 1024), // 64GB external/built-in storage total capacity
CreateTime: "2024-01-15T09:00:00Z",
},
{
EventType: "INTERNAL_STORAGE_MEASURED",
ByteCount: int64(10 * 1024 * 1024 * 1024), // 10GB free in system partition
CreateTime: "2024-01-15T10:00:00Z",
},
{
EventType: "EXTERNAL_STORAGE_MEASURED",
ByteCount: int64(25 * 1024 * 1024 * 1024), // 25GB free in external/built-in storage
CreateTime: "2024-01-15T10:00:00Z",
},
}
data, err := json.Marshal(deviceInfo)
require.NoError(t, err)
encodedData := base64.StdEncoding.EncodeToString(data)
return &android.PubSubMessage{
Attributes: map[string]string{
"notificationType": string(notificationType),
},
Data: encodedData,
}
}
type fmaTestState struct {
version string
installerBytes []byte
sha256 string
installerPath string
}
func (s *fmaTestState) ComputeSHA(b []byte) {
h := sha256.New()
h.Write(b)
s.sha256 = hex.EncodeToString(h.Sum(nil))
}
func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTestState) {
if len(states) == 0 {
states = make(map[string]*fmaTestState, 1)
states["/zoom/windows.json"] = &fmaTestState{
version: "1.0",
installerBytes: []byte("xyz"),
installerPath: "/zoom.msi",
}
}
statesByInstallerPath := make(map[string]*fmaTestState, len(states))
for _, state := range states {
state.ComputeSHA(state.installerBytes)
statesByInstallerPath[state.installerPath] = state
}
var downloadMu sync.Mutex
// Mock installer server — routes by path to serve per-FMA bytes.
installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
downloadMu.Lock()
defer downloadMu.Unlock()
state, found := statesByInstallerPath[r.URL.Path]
if !found {
http.NotFound(w, r)
return
}
_, _ = w.Write(state.installerBytes)
}))
// call Refresh directly (instead of SyncApps) since we're using the server above and not the file server
// created in SyncApps
err := maintained_apps.Refresh(t.Context(), ds, slog.New(slog.DiscardHandler))
require.NoError(t, err)
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var state *fmaTestState
state, found := states[r.URL.Path]
if !found {
http.NotFound(w, r)
return
}
versions := []*ma.FMAManifestApp{
{
Version: state.version,
Queries: ma.FMAQueries{Exists: "SELECT 1 FROM osquery_info;"},
InstallerURL: installerServer.URL + state.installerPath,
InstallScriptRef: "foobaz",
UninstallScriptRef: "foobaz",
SHA256: state.sha256,
DefaultCategories: []string{"Productivity"},
},
}
manifest := ma.FMAManifestFile{
Versions: versions,
Refs: map[string]string{"foobaz": "Hello World!"},
}
require.NoError(t, json.NewEncoder(w).Encode(manifest))
}))
t.Cleanup(func() {
manifestServer.Close()
installerServer.Close()
})
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t)
}