diff --git a/changes/17418-macos-14-nudge b/changes/17418-macos-14-nudge new file mode 100644 index 0000000000..cdf29816b9 --- /dev/null +++ b/changes/17418-macos-14-nudge @@ -0,0 +1 @@ +* macOS 14 and higher no longer display nudge notifications diff --git a/server/datastore/mysql/operating_systems.go b/server/datastore/mysql/operating_systems.go index 50bab6e30e..4b9fd0a2d6 100644 --- a/server/datastore/mysql/operating_systems.go +++ b/server/datastore/mysql/operating_systems.go @@ -45,6 +45,10 @@ func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, }) } +func (ds *Datastore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return getHostOperatingSystemDB(ctx, ds.reader(ctx), hostID) +} + // getOrGenerateOperatingSystemDB queries the `operating_systems` table with the // name, version, arch, and kernel_version of the given operating system. If found, // it returns the record including the associated ID. If not found, it returns a call @@ -168,11 +172,11 @@ func getIDHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID // getIDHostOperatingSystemDB queries the `operating_systems` table and returns the // operating system record associated with the given host ID based on a subquery // of the `host_operating_system` table. -func getHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint) (*fleet.OperatingSystem, error) { +func getHostOperatingSystemDB(ctx context.Context, tx sqlx.QueryerContext, hostID uint) (*fleet.OperatingSystem, error) { var os fleet.OperatingSystem stmt := "SELECT id, name, version, arch, kernel_version, platform, display_version, os_version_id FROM operating_systems WHERE id = (SELECT os_id FROM host_operating_system WHERE host_id = ?)" if err := sqlx.GetContext(ctx, tx, &os, stmt, hostID); err != nil { - return nil, err + return nil, ctxerr.Wrap(ctx, err, "getting host os") } return &os, nil } diff --git a/server/datastore/mysql/operating_systems_test.go b/server/datastore/mysql/operating_systems_test.go index f8bc2aa2be..5819d269a7 100644 --- a/server/datastore/mysql/operating_systems_test.go +++ b/server/datastore/mysql/operating_systems_test.go @@ -3,6 +3,7 @@ package mysql import ( "context" "database/sql" + "errors" "fmt" "sync" "testing" @@ -295,6 +296,9 @@ func TestGetHostOperatingSystem(t *testing.T) { _, err = getHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID) require.ErrorIs(t, err, sql.ErrNoRows) + _, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.ErrorIs(t, err, sql.ErrNoRows) + // insert test host and os id err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[0].ID) require.NoError(t, err) @@ -302,6 +306,10 @@ func TestGetHostOperatingSystem(t *testing.T) { require.NoError(t, err) require.Equal(t, osList[0], *os) + os, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.NoError(t, err) + require.Equal(t, osList[0], *os) + // update test host with new os id err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[1].ID) require.NoError(t, err) @@ -309,12 +317,20 @@ func TestGetHostOperatingSystem(t *testing.T) { require.NoError(t, err) require.Equal(t, osList[1], *os) + os, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1], *os) + // no change err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[1].ID) require.NoError(t, err) os, err = getHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID) require.NoError(t, err) require.Equal(t, osList[1], *os) + + os, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1], *os) } func TestCleanupHostOperatingSystems(t *testing.T) { @@ -352,7 +368,7 @@ func TestCleanupHostOperatingSystems(t *testing.T) { assertDeletedHostOS := func(expectDeletedIDs []uint) { for _, h := range testHosts { os, err := getHostOperatingSystemDB(ctx, ds.writer(ctx), h.ID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { require.Contains(t, expectDeletedIDs, h.ID) return } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 7d0b112a42..815c84bd7d 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -530,6 +530,9 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // OperatingSystemsStore + // GetHostOperatingSystem returns the operating system information + // for a given host. + GetHostOperatingSystem(ctx context.Context, hostID uint) (*OperatingSystem, error) // ListOperationsSystems returns all operating systems (id, name, version) ListOperatingSystems(ctx context.Context) ([]OperatingSystem, error) // ListOperatingSystemsForPlatform returns all operating systems for the given platform. diff --git a/server/fleet/operating_systems.go b/server/fleet/operating_systems.go index 19402c1633..91c9a1ead7 100644 --- a/server/fleet/operating_systems.go +++ b/server/fleet/operating_systems.go @@ -1,6 +1,10 @@ package fleet -import "strings" +import ( + "fmt" + "strconv" + "strings" +) // OperatingSystem is an operating system uniquely identified according to its name and version. type OperatingSystem struct { @@ -27,3 +31,23 @@ type OperatingSystem struct { func (os OperatingSystem) IsWindows() bool { return strings.ToLower(os.Platform) == "windows" } + +// RequiresNudge returns whether the target platform is darwin and +// below version 14. Starting at macOS 14 nudge is no longer required, +// as the mechanism to notify users about updates is built in. +func (os *OperatingSystem) RequiresNudge() (bool, error) { + if os.Platform != "darwin" { + return false, nil + } + + versionFloat, err := strconv.ParseFloat(os.Version, 32) + if err != nil { + return false, fmt.Errorf("parsing macos version \"%s\": %w", os.Version, err) + } + + if float32(versionFloat) < 14 { + return true, nil + } + + return false, nil +} diff --git a/server/fleet/operating_systems_test.go b/server/fleet/operating_systems_test.go index ea981c6175..3f737297b2 100644 --- a/server/fleet/operating_systems_test.go +++ b/server/fleet/operating_systems_test.go @@ -21,3 +21,35 @@ func TestOperatingSystemIsWindows(t *testing.T) { require.Equal(t, tc.isWindows, sut.IsWindows()) } } + +func TestOperatingSystemRequiresNudge(t *testing.T) { + testCases := []struct { + platform string + version string + requiresNudge bool + parseError bool + }{ + {platform: "chrome"}, + {platform: "chrome", version: "12.1"}, + {platform: "chrome", version: "15"}, + {platform: "darwin", parseError: true}, + {platform: "darwin", version: "12.0", requiresNudge: true}, + {platform: "darwin", version: "11", requiresNudge: true}, + {platform: "darwin", version: "14.0"}, + {platform: "darwin", version: "14.3"}, + {platform: "windows"}, + {platform: "windows", version: "12.2"}, + {platform: "windows", version: "15.4"}, + } + + for _, tc := range testCases { + os := OperatingSystem{Platform: tc.platform, Version: tc.version} + req, err := os.RequiresNudge() + if tc.parseError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.requiresNudge, req) + } +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index c42c08ee6c..1507ea6856 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -392,6 +392,8 @@ type InsertCVEMetaFunc func(ctx context.Context, cveMeta []fleet.CVEMeta) error type ListCVEsFunc func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) +type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) + type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error) type ListOperatingSystemsForPlatformFunc func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) @@ -1460,6 +1462,9 @@ type DataStore struct { ListCVEsFunc ListCVEsFunc ListCVEsFuncInvoked bool + GetHostOperatingSystemFunc GetHostOperatingSystemFunc + GetHostOperatingSystemFuncInvoked bool + ListOperatingSystemsFunc ListOperatingSystemsFunc ListOperatingSystemsFuncInvoked bool @@ -3531,6 +3536,13 @@ func (s *DataStore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]fleet return s.ListCVEsFunc(ctx, maxAge) } +func (s *DataStore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + s.mu.Lock() + s.GetHostOperatingSystemFuncInvoked = true + s.mu.Unlock() + return s.GetHostOperatingSystemFunc(ctx, hostID) +} + func (s *DataStore) ListOperatingSystems(ctx context.Context) ([]fleet.OperatingSystem, error) { s.mu.Lock() s.ListOperatingSystemsFuncInvoked = true diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 92c1053185..6ab2449cc6 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -7374,6 +7374,10 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { // nudge config is empty if macos_updates is not set, and Windows MDM notifications are unset h := createOrbitEnrolledHost(t, "darwin", "h", s.ds) + + err := s.ds.UpdateHostOperatingSystem(context.Background(), h.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"}) + require.NoError(t, err) + resp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp) require.Empty(t, resp.NudgeConfig) @@ -7402,7 +7406,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { }) mdmDevice.SerialNumber = h.HardwareSerial mdmDevice.UUID = h.UUID - err := mdmDevice.Enroll() + err = mdmDevice.Enroll() require.NoError(t, err) resp = orbitGetConfigResponse{} @@ -7459,12 +7463,54 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { mdmDevice.UUID = h2.UUID err = mdmDevice.Enroll() require.NoError(t, err) + + err = s.ds.UpdateHostOperatingSystem(context.Background(), h2.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"}) + require.NoError(t, err) + resp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h2.OrbitNodeKey)), http.StatusOK, &resp) wantCfg, err = fleet.NewNudgeConfig(fleet.MacOSUpdates{Deadline: optjson.SetString("2022-01-04"), MinimumVersion: optjson.SetString("12.1.3")}) require.NoError(t, err) require.Equal(t, wantCfg, resp.NudgeConfig) require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC") + + // host on macos > 14, shouldn't be receiving nudge configs + h3 := createOrbitEnrolledHost(t, "darwin", "h3", s.ds) + + mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + }) + mdmDevice.SerialNumber = h3.HardwareSerial + mdmDevice.UUID = h3.UUID + err = mdmDevice.Enroll() + require.NoError(t, err) + + err = s.ds.UpdateHostOperatingSystem(context.Background(), h3.ID, fleet.OperatingSystem{Platform: "darwin", Version: "14.1"}) + require.NoError(t, err) + + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h3.OrbitNodeKey)), http.StatusOK, &resp) + require.Nil(t, resp.NudgeConfig) + + // host is available for nudge, but has not had details query run + // yet, so we don't know the os version. + h4 := createOrbitEnrolledHost(t, "darwin", "h4", s.ds) + + mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + }) + mdmDevice.SerialNumber = h4.HardwareSerial + mdmDevice.UUID = h4.UUID + err = mdmDevice.Enroll() + require.NoError(t, err) + + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h4.OrbitNodeKey)), http.StatusOK, &resp) + require.Nil(t, resp.NudgeConfig) } func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() { @@ -8935,7 +8981,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { stmt := ` SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, COALESCE(label_id, 0) as label_id - FROM mdm_configuration_profile_labels + FROM mdm_configuration_profile_labels UNION SELECT apple_declaration_uuid as profile_uuid, label_name, COALESCE(label_id, 0) as label_id FROM mdm_declaration_labels ORDER BY profile_uuid, label_name;` return sqlx.SelectContext(context.Background(), q, &profileLabels, stmt) diff --git a/server/service/orbit.go b/server/service/orbit.go index 63809ba902..05230cabeb 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -2,6 +2,7 @@ package service import ( "context" + "database/sql" "encoding/json" "errors" "fmt" @@ -268,10 +269,24 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro if appConfig.MDM.EnabledAndConfigured && mdmConfig != nil && mdmConfig.MacOSUpdates.EnabledForHost(host) { - nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates) + hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID) + if errors.Is(err, sql.ErrNoRows) { + // host os has not been collected yet (no details query) + hostOS = &fleet.OperatingSystem{} + } else if err != nil { + return fleet.OrbitConfig{}, err + } + requiresNudge, err := hostOS.RequiresNudge() if err != nil { return fleet.OrbitConfig{}, err } + + if requiresNudge { + nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates) + if err != nil { + return fleet.OrbitConfig{}, err + } + } } if mdmConfig.EnableDiskEncryption && @@ -313,10 +328,25 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro var nudgeConfig *fleet.NudgeConfig if appConfig.MDM.EnabledAndConfigured && appConfig.MDM.MacOSUpdates.EnabledForHost(host) { - nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates) + hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID) + if errors.Is(err, sql.ErrNoRows) { + // host os has not been collected yet (no details query) + hostOS = &fleet.OperatingSystem{} + } else if err != nil { + return fleet.OrbitConfig{}, err + } + requiresNudge, err := hostOS.RequiresNudge() if err != nil { return fleet.OrbitConfig{}, err } + + if requiresNudge { + nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates) + if err != nil { + return fleet.OrbitConfig{}, err + + } + } } if appConfig.MDM.WindowsEnabledAndConfigured && diff --git a/server/service/orbit_test.go b/server/service/orbit_test.go index b294983de1..97a203d86b 100644 --- a/server/service/orbit_test.go +++ b/server/service/orbit_test.go @@ -22,11 +22,19 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return appCfg, nil } + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { return nil, nil } ctx = test.HostContext(ctx, &fleet.Host{ OsqueryHostID: ptr.String("test"), + ID: 1, MDMInfo: &fleet.HostMDM{ IsServer: false, InstalledFromDep: true, @@ -65,7 +73,13 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return appCfg, nil } - + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } team := fleet.Team{ID: 1} teamMDM := fleet.TeamMDM{} ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { @@ -81,6 +95,7 @@ func TestGetOrbitConfigNudge(t *testing.T) { ctx = test.HostContext(ctx, &fleet.Host{ OsqueryHostID: ptr.String("test"), + ID: 1, TeamID: ptr.Uint(team.ID), MDMInfo: &fleet.HostMDM{ IsServer: false, @@ -120,6 +135,13 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds := new(mock.Store) license := &fleet.LicenseInfo{Tier: fleet.TierPremium} svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}} appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("2022-04-01") @@ -193,4 +215,103 @@ func TestGetOrbitConfigNudge(t *testing.T) { }}) }) + + t.Run("no-nudge on macos versions greater than 14", func(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + MDMInfo: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMFleet, + }} + + team := fleet.Team{ID: 1} + teamMDM := fleet.TeamMDM{} + teamMDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") + teamMDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.1") + ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { + require.Equal(t, team.ID, teamID) + return &teamMDM, nil + } + ds.TeamAgentOptionsFunc = func(ctx context.Context, id uint) (*json.RawMessage, error) { + return ptr.RawMessage(json.RawMessage(`{}`)), nil + } + ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { + return nil, nil + } + + appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}} + appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") + appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.3") + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appCfg, nil + } + ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { + return nil, nil + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } + ctx = test.HostContext(ctx, host) + + // Version < 14 gets nudge + host.ID = 1 + cfg, err := svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.NotEmpty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // Version > 14 gets no nudge + os.Version = "14.1" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // windows gets no nudge + os.Platform = "windows" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + //// team section below + host.TeamID = ptr.Uint(team.ID) + os.Platform = "darwin" + os.Version = "12.1" + + // Version < 14 gets nudge + host.ID = 1 + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.NotEmpty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // Version > 14 gets no nudge + os.Version = "14.1" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // windows gets no nudge + os.Platform = "windows" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + }) }