mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- 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 -->
1503 lines
44 KiB
Go
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)
|
|
}
|