From c213020d0b31d4b75edcb86242c9acb04c267e68 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 15 Nov 2024 12:43:38 -0600 Subject: [PATCH 01/36] Frontend changes for NDES issue #23525 (#23829) (#23852) Cherry-pick of #23829 --- .../cards/MdmSettings/ScepPage/ScepPage.tsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/ScepPage/ScepPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/ScepPage/ScepPage.tsx index dc1874e637a1..caea834d89e3 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/ScepPage/ScepPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/ScepPage/ScepPage.tsx @@ -29,9 +29,15 @@ const baseClass = "scep-page"; const BAD_SCEP_URL_ERROR = "Invalid SCEP URL. Please correct and try again."; const BAD_CREDENTIALS_ERROR = - "Invalid admin URL or credentials. Please correct and try again."; + "Couldn't add. Admin URL or credentials are invalid."; const CACHE_ERROR = "The NDES password cache is full. Please increase the number of cached passwords in NDES and try again. By default, NDES caches 5 passwords and they expire 60 minutes after they are created."; +const INSUFFICIENT_PERMISSIONS_ERROR = + "Couldn't add. This account doesn't have sufficient permissions. Please use the account with enroll permission."; +const SCEP_URL_TIMEOUT_ERROR = + "Couldn't add. Request to NDES (SCEP URL) timed out. Please try again."; +const ADMIN_URL_TIMEOUT_ERROR = + "Couldn't add. Request to NDES (admin URL) timed out. Please try again."; const DEFAULT_ERROR = "Something went wrong updating your SCEP server. Please try again."; @@ -348,12 +354,24 @@ const ScepPage = ({ router }: IScepPageProps) => { } catch (error) { console.error(error); const reason = getErrorReason(error); - if (reason.includes("invalid SCEP URL")) { - renderFlash("error", BAD_SCEP_URL_ERROR); - } else if (reason.includes("invalid admin URL or credentials")) { + if (reason.includes("invalid admin URL or credentials")) { renderFlash("error", BAD_CREDENTIALS_ERROR); } else if (reason.includes("the password cache is full")) { renderFlash("error", CACHE_ERROR); + } else if (reason.includes("does not have sufficient permissions")) { + renderFlash("error", INSUFFICIENT_PERMISSIONS_ERROR); + } else if ( + reason.includes(formData.scepUrl) && + reason.includes("context deadline exceeded") + ) { + renderFlash("error", SCEP_URL_TIMEOUT_ERROR); + } else if ( + reason.includes(formData.adminUrl) && + reason.includes("context deadline exceeded") + ) { + renderFlash("error", ADMIN_URL_TIMEOUT_ERROR); + } else if (reason.includes("invalid SCEP URL")) { + renderFlash("error", BAD_SCEP_URL_ERROR); } else renderFlash("error", DEFAULT_ERROR); } finally { setIsUpdatingNdesScepProxy(false); From 13ca79f0f8fdc22d8d054d8c83000f34ceaa332e Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Fri, 15 Nov 2024 14:38:41 -0500 Subject: [PATCH 02/36] fix: re-enroll devices that are removed from ABM and then added back (#23757) (#23835) Cherry pick for #23757 --- changes/23200-ade-enroll | 2 + server/datastore/mysql/hosts.go | 36 +++ server/datastore/mysql/hosts_test.go | 81 +++++++ server/fleet/datastore.go | 5 + server/mdm/apple/apple_mdm.go | 17 +- server/mock/datastore_mock.go | 12 + server/service/integration_mdm_dep_test.go | 268 +++++++++++++++++++-- server/service/integration_mdm_test.go | 4 +- 8 files changed, 397 insertions(+), 28 deletions(-) create mode 100644 changes/23200-ade-enroll diff --git a/changes/23200-ade-enroll b/changes/23200-ade-enroll new file mode 100644 index 000000000000..6a6c597bf480 --- /dev/null +++ b/changes/23200-ade-enroll @@ -0,0 +1,2 @@ +- Fixes a bug where a device that was removed from ABM and then added back wouldn't properly + re-enroll in Fleet MDM \ No newline at end of file diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 4a9af648f668..d259e914098a 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -5155,6 +5155,42 @@ func (ds *Datastore) GetMatchingHostSerials(ctx context.Context, serials []strin return result, nil } +func (ds *Datastore) GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) { + result := map[string]struct{}{} + if len(serials) == 0 { + return result, nil + } + + stmt := ` +SELECT + hardware_serial +FROM + hosts h + JOIN host_dep_assignments hdep ON hdep.host_id = h.id +WHERE + h.hardware_serial IN (?) AND hdep.deleted_at IS NOT NULL; + ` + + var args []interface{} + for _, serial := range serials { + args = append(args, serial) + } + stmt, args, err := sqlx.In(stmt, args) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "building IN statement for matching hosts") + } + var matchingSerials []string + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &matchingSerials, stmt, args...); err != nil { + return nil, err + } + + for _, serial := range matchingSerials { + result[serial] = struct{}{} + } + + return result, nil +} + func (ds *Datastore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHealth, error) { sqlStmt := ` SELECT h.os_version, h.updated_at, h.platform, h.team_id, hd.encrypted as disk_encryption_enabled FROM hosts h diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index bb82622b6a28..fd41dc45e860 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -170,6 +170,7 @@ func TestHosts(t *testing.T) { {"UpdateHostIssues", testUpdateHostIssues}, {"ListUpcomingHostMaintenanceWindows", testListUpcomingHostMaintenanceWindows}, {"GetHostEmails", testGetHostEmails}, + {"TestGetMatchingHostSerialsMarkedDeleted", testGetMatchingHostSerialsMarkedDeleted}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -9751,3 +9752,83 @@ func testGetHostEmails(t *testing.T, ds *Datastore) { require.NoError(t, err) assert.ElementsMatch(t, []string{"foo@example.com", "bar@example.com"}, emails) } + +func testGetMatchingHostSerialsMarkedDeleted(t *testing.T, ds *Datastore) { + ctx := context.Background() + serials := []string{"foo", "bar", "baz"} + team, err := ds.NewTeam(context.Background(), &fleet.Team{ + Name: "team1", + }) + require.NoError(t, err) + abmTok, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: t.Name(), EncryptedToken: []byte("token")}) + require.NoError(t, err) + var hosts []fleet.Host + for i, serial := range serials { + var tmID *uint + if serial == "bar" { + tmID = &team.ID + } + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(fmt.Sprint(i)), + UUID: fmt.Sprint(i), + OsqueryHostID: ptr.String(fmt.Sprint(i)), + Hostname: "foo.local", + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + HardwareSerial: serial, + TeamID: tmID, + ID: uint(i), + }) + require.NoError(t, err) + require.NotNil(t, h) + + // Only "foo" and "baz" are + if i%2 == 0 { + hosts = append(hosts, *h) + } + } + + require.NoError(t, ds.UpsertMDMAppleHostDEPAssignments(ctx, hosts, abmTok.ID)) + require.NoError(t, ds.DeleteHostDEPAssignments(ctx, abmTok.ID, serials)) + + cases := []struct { + name string + in []string + want map[string]struct{} + err string + }{ + {"no serials provided", []string{}, map[string]struct{}{}, ""}, + {"no matching serials", []string{"oof", "rab", "bar"}, map[string]struct{}{}, ""}, + { + "partial matches", + []string{"foo", "rab", "bar"}, + map[string]struct{}{"foo": {}}, + "", + }, + { + "all matching", + []string{"foo", "baz"}, + map[string]struct{}{ + "foo": {}, + "baz": {}, + }, + "", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got, err := ds.GetMatchingHostSerialsMarkedDeleted(ctx, tt.in) + if tt.err == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.err) + } + require.Equal(t, tt.want, got) + }) + } +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index c05e5ac4fcd5..c962d4119c09 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1298,6 +1298,11 @@ type Datastore interface { // a map that only contains the serials that have a matching row in the `hosts` table. GetMatchingHostSerials(ctx context.Context, serials []string) (map[string]*Host, error) + // GetMatchingHostSerialsMarkedDeleted takes a list of device serial numbers and returns a map + // of only the ones that were found in the `hosts` table AND have a row in + // `host_dep_assignments` that is marked as deleted. + GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) + // DeleteHostDEPAssignmentsFromAnotherABM makes as deleted any DEP entry that matches one of the provided serials only if the entry is NOT associated to the provided ABM token. DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 49e1125f247a..5ea71f3aa284 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -660,6 +660,18 @@ func (d *DEPService) processDeviceResponse( for _, device := range addedDevicesSlice { addedSerials = append(addedSerials, device.SerialNumber) } + + // Check if any of the "added" or "modified" hosts are hosts that we've recently removed from + // Fleet in ABM. A host in this state will have a row in `host_dep_assignments` where the + // `deleted_at ` col is NOT NULL. Down below we skip assigning the profile to devices that we + // think are still enrolled; doing this check here allows us to avoid skipping devices that + // _seem_ like they're still enrolled but were actually removed and should get the profile. + // See https://github.com/fleetdm/fleet/issues/23200 for more context. + existingDeletedSerials, err := d.ds.GetMatchingHostSerialsMarkedDeleted(ctx, addedSerials) + if err != nil { + return ctxerr.Wrap(ctx, err, "get matching deleted host serials") + } + err = d.ds.DeleteHostDEPAssignmentsFromAnotherABM(ctx, abmTokenID, addedSerials) if err != nil { return ctxerr.Wrap(ctx, err, "deleting dep assignments from another abm") @@ -682,7 +694,7 @@ func (d *DEPService) processDeviceResponse( } level.Debug(kitlog.With(d.logger)).Log("msg", "devices to assign DEP profiles", "to_add", len(addedDevicesSlice), "to_remove", - deletedSerials, "to_modify", modifiedSerials) + strings.Join(deletedSerials, ", "), "to_modify", strings.Join(modifiedSerials, ", ")) // at this point, the hosts rows are created for the devices, with the // correct team_id, so we know what team-specific profile needs to be applied. @@ -754,7 +766,8 @@ func (d *DEPService) processDeviceResponse( for profUUID, devices := range profileToDevices { var serials []string for _, device := range devices { - if device.ProfileUUID == profUUID { + _, ok := existingDeletedSerials[device.SerialNumber] + if device.ProfileUUID == profUUID && !ok { skippedSerials = append(skippedSerials, device.SerialNumber) continue } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 48cc6d60766a..ad49d7a6aa28 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -859,6 +859,8 @@ type GetMDMAppleDefaultSetupAssistantFunc func(ctx context.Context, teamID *uint type GetMatchingHostSerialsFunc func(ctx context.Context, serials []string) (map[string]*fleet.Host, error) +type GetMatchingHostSerialsMarkedDeletedFunc func(ctx context.Context, serials []string) (map[string]struct{}, error) + type DeleteHostDEPAssignmentsFromAnotherABMFunc func(ctx context.Context, abmTokenID uint, serials []string) error type DeleteHostDEPAssignmentsFunc func(ctx context.Context, abmTokenID uint, serials []string) error @@ -2405,6 +2407,9 @@ type DataStore struct { GetMatchingHostSerialsFunc GetMatchingHostSerialsFunc GetMatchingHostSerialsFuncInvoked bool + GetMatchingHostSerialsMarkedDeletedFunc GetMatchingHostSerialsMarkedDeletedFunc + GetMatchingHostSerialsMarkedDeletedFuncInvoked bool + DeleteHostDEPAssignmentsFromAnotherABMFunc DeleteHostDEPAssignmentsFromAnotherABMFunc DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked bool @@ -5773,6 +5778,13 @@ func (s *DataStore) GetMatchingHostSerials(ctx context.Context, serials []string return s.GetMatchingHostSerialsFunc(ctx, serials) } +func (s *DataStore) GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) { + s.mu.Lock() + s.GetMatchingHostSerialsMarkedDeletedFuncInvoked = true + s.mu.Unlock() + return s.GetMatchingHostSerialsMarkedDeletedFunc(ctx, serials) +} + func (s *DataStore) DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error { s.mu.Lock() s.DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked = true diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 50ec5112cc17..d2a32c8e33cf 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -679,18 +679,19 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { } type hostDEPRow struct { - HostID uint `db:"host_id"` - ProfileUUID string `db:"profile_uuid"` - AssignProfileResponse string `db:"assign_profile_response"` - ResponseUpdatedAt time.Time `db:"response_updated_at"` - RetryJobID uint `db:"retry_job_id"` + HostID uint `db:"host_id"` + ProfileUUID string `db:"profile_uuid"` + AssignProfileResponse string `db:"assign_profile_response"` + ResponseUpdatedAt time.Time `db:"response_updated_at"` + RetryJobID uint `db:"retry_job_id"` + DeletedAt *time.Time `db:"deleted_at"` } checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { bySerial := make(map[string]hostDEPRow, len(deviceSerials)) for _, deviceSerial := range deviceSerials { mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { var dest hostDEPRow - err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) + err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id, deleted_at FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) require.NoError(t, err) require.Equal(t, string(expectedStatus), dest.AssignProfileResponse) bySerial[deviceSerial] = dest @@ -1046,14 +1047,22 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { deletedSerial = devices[1].SerialNumber devices = []godep.Device{ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now()}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified", - OpDate: time.Now().Add(time.Second)}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(2 * time.Second)}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", - OpDate: time.Now().Add(3 * time.Second)}, - {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(4 * time.Second)}, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified", + OpDate: time.Now().Add(time.Second), + }, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(2 * time.Second), + }, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", + OpDate: time.Now().Add(3 * time.Second), + }, + { + SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(4 * time.Second), + }, {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()}, {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now().Add(time.Second)}, @@ -1436,7 +1445,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { RetryJobID uint `db:"retry_job_id"` } checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, - expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { + expectedStatus fleet.DEPAssignProfileResponseStatus, + ) map[string]hostDEPRow { bySerial := make(map[string]hostDEPRow, len(deviceSerials)) for _, deviceSerial := range deviceSerials { mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { @@ -1627,14 +1637,18 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { devices = []godep.Device{ {SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "added", OpDate: time.Now()}, {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", OpDate: time.Now()}, - {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added", + OpDate: time.Now().Add(time.Microsecond), + }, } defaultOrgDevices = []godep.Device{ {SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "added", OpDate: time.Now()}, {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", OpDate: time.Now()}, - {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added", + OpDate: time.Now().Add(time.Microsecond), + }, } // trigger a profile sync @@ -1663,13 +1677,17 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { // Delete the devices devices = []godep.Device{ {SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "modified", OpDate: time.Now()}, - {SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(time.Microsecond), + }, } defaultOrgDevices = []godep.Device{ {SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "modified", OpDate: time.Now()}, - {SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", - OpDate: time.Now().Add(time.Microsecond)}, + { + SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", + OpDate: time.Now().Add(time.Microsecond), + }, } // trigger a profile sync @@ -1694,7 +1712,6 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() { checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess) checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess) - } func (s *integrationMDMTestSuite) TestDeprecatedDefaultAppleBMTeam() { @@ -2342,3 +2359,206 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo require.Equal(t, 1, deviceConfiguredCount) require.Equal(t, 0, otherCount) } + +func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM() { + t := s.T() + s.enableABM(t.Name()) + ctx := context.Background() + + checkPostEnrollmentCommands := func(mdmDevice *mdmtest.TestAppleMDMClient, shouldReceive bool) { + // run the worker to process the DEP enroll request + s.runWorker() + // run the worker to assign configuration profiles + s.awaitTriggerProfileSchedule(t) + + var fleetdCmd, installProfileCmd *micromdm.CommandPayload + cmd, err := mdmDevice.Idle() + require.NoError(t, err) + for cmd != nil { + var fullCmd micromdm.CommandPayload + require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd)) + if fullCmd.Command.RequestType == "InstallEnterpriseApplication" && + fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil && + strings.Contains(*fullCmd.Command.InstallEnterpriseApplication.ManifestURL, fleetdbase.GetPKGManifestURL()) { + fleetdCmd = &fullCmd + } else if cmd.Command.RequestType == "InstallProfile" { + installProfileCmd = &fullCmd + } + cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID) + require.NoError(t, err) + } + + if shouldReceive { + // received request to install fleetd + require.NotNil(t, fleetdCmd, "host didn't get a command to install fleetd") + require.NotNil(t, fleetdCmd.Command, "host didn't get a command to install fleetd") + + // received request to install the global configuration profile + require.NotNil(t, installProfileCmd, "host didn't get a command to install profiles") + require.NotNil(t, installProfileCmd.Command, "host didn't get a command to install profiles") + } else { + require.Nil(t, fleetdCmd, "host got a command to install fleetd") + require.Nil(t, installProfileCmd, "host got a command to install profiles") + } + } + + devices := []godep.Device{ + {SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"}, + } + + profileAssignmentReqs := []profileAssignmentReq{} + + type hostDEPRow struct { + HostID uint `db:"host_id"` + ProfileUUID string `db:"profile_uuid"` + AssignProfileResponse string `db:"assign_profile_response"` + ResponseUpdatedAt time.Time `db:"response_updated_at"` + RetryJobID uint `db:"retry_job_id"` + DeletedAt *time.Time `db:"deleted_at"` + } + checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { + bySerial := make(map[string]hostDEPRow, len(deviceSerials)) + for _, deviceSerial := range deviceSerials { + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var dest hostDEPRow + err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id, deleted_at FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) + require.NoError(t, err) + require.Equal(t, string(expectedStatus), dest.AssignProfileResponse) + bySerial[deviceSerial] = dest + return nil + }) + } + return bySerial + } + + s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + encoder := json.NewEncoder(w) + switch r.URL.Path { + case "/session": + err := encoder.Encode(map[string]string{"auth_session_token": "xyz"}) + require.NoError(t, err) + case "/profile": + err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()}) + require.NoError(t, err) + case "/server/devices": + // This endpoint is used to get an initial list of + // devices, return a single device + err := encoder.Encode(godep.DeviceResponse{Devices: devices[:1]}) + require.NoError(t, err) + case "/devices/sync": + // This endpoint is polled over time to sync devices from + // ABM, send a repeated serial and a new one + err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"}) + require.NoError(t, err) + case "/profile/devices": + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var prof profileAssignmentReq + require.NoError(t, json.Unmarshal(b, &prof)) + profileAssignmentReqs = append(profileAssignmentReqs, prof) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = make(map[string]string, len(prof.Devices)) + for _, device := range prof.Devices { + resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess) + } + err = encoder.Encode(resp) + require.NoError(t, err) + default: + _, _ = w.Write([]byte(`{}`)) + } + })) + + s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) { + return map[string]*push.Response{}, nil + } + + // Enroll the host via ADE + depURLToken := loadEnrollmentProfileDEPToken(t, s.ds) + mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken) + mdmDevice.SerialNumber = devices[0].SerialNumber + err := mdmDevice.Enroll() + require.NoError(t, err) + + // Simulate an osquery enrollment too + // set an enroll secret + var applyResp applyEnrollSecretSpecResponse + s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{ + Spec: &fleet.EnrollSecretSpec{ + Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}}, + }, + }, http.StatusOK, &applyResp) + + // simulate a matching host enrolling via osquery + j, err := json.Marshal(&enrollAgentRequest{ + EnrollSecret: t.Name(), + HostIdentifier: mdmDevice.UUID, + }) + require.NoError(t, err) + var enrollResp enrollAgentResponse + hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK) + defer hres.Body.Close() + require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp)) + require.NotEmpty(t, enrollResp.NodeKey) + + listHostsRes := listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes) + require.Len(t, listHostsRes.Hosts, 1) + h := listHostsRes.Hosts[0] + + s.runDEPSchedule() + + // make sure the host gets post enrollment requests + checkPostEnrollmentCommands(mdmDevice, true) + + var hostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp) + // 1 profile with fleetd configuration + 1 root CA config + require.Len(t, *hostResp.Host.MDM.Profiles, 2) + + // Turn MDM off in the host + s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusOK) + + // profiles are removed and the host is no longer enrolled + hostResp = getHostResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp) + require.Nil(t, hostResp.Host.MDM.Profiles) + require.Equal(t, "", hostResp.Host.MDM.Name) + + err = mdmDevice.Checkout() + require.NoError(t, err) + + // Simulate the device getting unassigned from Fleet in ABM + devices = []godep.Device{ + {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()}, + } + + t.Log("RUN AFTER DELETED") + s.runDEPSchedule() + + a := checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + require.NotZero(t, a[mdmDevice.SerialNumber].DeletedAt) + + // Now we add the device back into ABM + profileAssignmentReqs = []profileAssignmentReq{} + + devices = []godep.Device{ + // In https://github.com/fleetdm/fleet/issues/23200, we saw a profileUUID being sent back on + // the godep.Device in the response from ABM. We're not 100% sure why, but the fact that + // this field is set was the source of the bug, which is why we're including it here. + {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now(), ProfileUUID: a[mdmDevice.SerialNumber].ProfileUUID}, + } + + t.Log("RUN AFTER RE-ADDED") + s.runDEPSchedule() + + a = checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + require.Nil(t, a[mdmDevice.SerialNumber].DeletedAt) + + err = mdmDevice.Enroll() + require.NoError(t, err) + + // make sure the host gets post enrollment requests + checkPostEnrollmentCommands(mdmDevice, true) +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 0e51ef7cc18e..2d985423286a 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11914,7 +11914,7 @@ func (s *integrationMDMTestSuite) TestSetupExperience() { require.True(t, vppFound, "vpp app not found in status results") require.True(t, softwareFound, "software installer app not found in status results") - x, err := s.ds.GetHostAwaitingConfiguration(ctx, fleetHost.UUID) + awaitingConfig, err := s.ds.GetHostAwaitingConfiguration(ctx, fleetHost.UUID) require.NoError(t, err) - require.True(t, x) + require.True(t, awaitingConfig) } From 468a5b818f4903e54a98c7ee0c1e001b5967aed8 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 15 Nov 2024 19:41:46 -0300 Subject: [PATCH 03/36] Cherry pick PR #23766 into RC v4.60.0 (#23865) Cherry pick PR #23766 into RC v4.60.0 Co-authored-by: Tim Lee Co-authored-by: Ian Littman --- changes/8750-add-team_identifier-to-software | 1 + .../Contributing/Understanding-host-vitals.md | 42 ++++--- orbit/changes/add-codesign-table | 1 + orbit/pkg/table/codesign/codesign_darwin.go | 100 +++++++++++++++++ orbit/pkg/table/extension_darwin.go | 3 + schema/osquery_fleet_schema.json | 25 +++++ schema/tables/codesign.yml | 15 +++ ...mIdentifierToHostSoftwareInstalledPaths.go | 23 ++++ server/datastore/mysql/schema.sql | 7 +- server/datastore/mysql/software.go | 70 ++++++++---- server/datastore/mysql/software_test.go | 22 ++-- server/fleet/datastore.go | 5 +- server/fleet/hosts.go | 3 + server/fleet/software.go | 19 +++- server/fleet/software_installer.go | 19 ++-- server/service/integration_core_test.go | 105 ++++++++++++++++++ server/service/osquery.go | 10 +- server/service/osquery_test.go | 1 + server/service/osquery_utils/queries.go | 81 +++++++++----- server/service/osquery_utils/queries_test.go | 4 +- 20 files changed, 455 insertions(+), 101 deletions(-) create mode 100644 changes/8750-add-team_identifier-to-software create mode 100644 orbit/changes/add-codesign-table create mode 100644 orbit/pkg/table/codesign/codesign_darwin.go create mode 100644 schema/tables/codesign.yml create mode 100644 server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go diff --git a/changes/8750-add-team_identifier-to-software b/changes/8750-add-team_identifier-to-software new file mode 100644 index 000000000000..0d05d81b0944 --- /dev/null +++ b/changes/8750-add-team_identifier-to-software @@ -0,0 +1 @@ +* Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint. diff --git a/docs/Contributing/Understanding-host-vitals.md b/docs/Contributing/Understanding-host-vitals.md index c1c6d2b0c9d1..aa3670ee63be 100644 --- a/docs/Contributing/Understanding-host-vitals.md +++ b/docs/Contributing/Understanding-host-vitals.md @@ -480,7 +480,6 @@ SELECT version AS version, identifier AS extension_id, browser_type AS browser, - 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source, '' AS vendor, '' AS installed_path @@ -500,7 +499,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups) SELECT name AS name, version AS version, - 'Package (deb)' AS type, '' AS extension_id, '' AS browser, 'deb_packages' AS source, @@ -514,7 +512,6 @@ UNION SELECT package AS name, version AS version, - 'Package (Portage)' AS type, '' AS extension_id, '' AS browser, 'portage_packages' AS source, @@ -527,7 +524,6 @@ UNION SELECT name AS name, version AS version, - 'Package (RPM)' AS type, '' AS extension_id, '' AS browser, 'rpm_packages' AS source, @@ -540,7 +536,6 @@ UNION SELECT name AS name, version AS version, - 'Package (NPM)' AS type, '' AS extension_id, '' AS browser, 'npm_packages' AS source, @@ -553,7 +548,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Chrome)' AS type, identifier AS extension_id, browser_type AS browser, 'chrome_extensions' AS source, @@ -566,7 +560,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Firefox)' AS type, identifier AS extension_id, 'firefox' AS browser, 'firefox_addons' AS source, @@ -579,7 +572,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Python)' AS type, '' AS extension_id, '' AS browser, 'python_packages' AS source, @@ -603,7 +595,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups) SELECT name AS name, COALESCE(NULLIF(bundle_short_version, ''), bundle_version) AS version, - 'Application (macOS)' AS type, bundle_identifier AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -616,7 +607,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Python)' AS type, '' AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -629,7 +619,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Chrome)' AS type, '' AS bundle_identifier, identifier AS extension_id, browser_type AS browser, @@ -642,7 +631,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Firefox)' AS type, '' AS bundle_identifier, identifier AS extension_id, 'firefox' AS browser, @@ -655,7 +643,6 @@ UNION SELECT name As name, version AS version, - 'Browser plugin (Safari)' AS type, '' AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -668,7 +655,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Homebrew)' AS type, '' AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -679,9 +665,27 @@ SELECT FROM homebrew_packages; ``` +## software_macos_codesign + +- Description: A software override query[^1] to append codesign information to macOS software entries. Requires `fleetd` + +- Platforms: darwin + +- Discovery query: +```sql +SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'codesign' +``` + +- Query: +```sql +SELECT a.path, c.team_identifier + FROM apps a + JOIN codesign c ON a.path = c.path +``` + ## software_macos_firefox -- Description: A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd` +- Description: A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd` - Platforms: darwin @@ -709,7 +713,6 @@ WITH app_paths AS ( ELSE 'Firefox.app' END AS name, COALESCE(NULLIF(apps.bundle_short_version, ''), apps.bundle_version) AS version, - 'Application (macOS)' AS type, apps.bundle_identifier AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -740,7 +743,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups) SELECT name, version, - 'IDE extension (VS Code)' AS type, '' AS bundle_identifier, uuid AS extension_id, '' AS browser, @@ -764,7 +766,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups) SELECT name AS name, version AS version, - 'Program (Windows)' AS type, '' AS extension_id, '' AS browser, 'programs' AS source, @@ -775,7 +776,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Python)' AS type, '' AS extension_id, '' AS browser, 'python_packages' AS source, @@ -786,7 +786,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (IE)' AS type, '' AS extension_id, '' AS browser, 'ie_extensions' AS source, @@ -797,7 +796,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Chrome)' AS type, identifier AS extension_id, browser_type AS browser, 'chrome_extensions' AS source, @@ -808,7 +806,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Firefox)' AS type, identifier AS extension_id, 'firefox' AS browser, 'firefox_addons' AS source, @@ -819,7 +816,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Chocolatey)' AS type, '' AS extension_id, '' AS browser, 'chocolatey_packages' AS source, diff --git a/orbit/changes/add-codesign-table b/orbit/changes/add-codesign-table new file mode 100644 index 000000000000..49b38025d636 --- /dev/null +++ b/orbit/changes/add-codesign-table @@ -0,0 +1 @@ +* Added `codesign` table to provide the "Team identifier" of macOS applications. diff --git a/orbit/pkg/table/codesign/codesign_darwin.go b/orbit/pkg/table/codesign/codesign_darwin.go new file mode 100644 index 000000000000..e1e8b26c9abd --- /dev/null +++ b/orbit/pkg/table/codesign/codesign_darwin.go @@ -0,0 +1,100 @@ +//go:build darwin +// +build darwin + +// Package codesign implements an extension osquery table +// to get signature information of macOS applications. +package codesign + +import ( + "bufio" + "bytes" + "context" + "errors" + "os/exec" + "strings" + + "github.com/osquery/osquery-go/plugin/table" + "github.com/rs/zerolog/log" +) + +// Columns is the schema of the table. +func Columns() []table.ColumnDefinition { + return []table.ColumnDefinition{ + // path is the absolute path to the app bundle. + // It's required and only supports the equality operator. + table.TextColumn("path"), + // team_identifier is the "Team ID", aka "Signature ID", "Developer ID". + // The value is "" if the app doesn't have a team identifier set. + // (this is the case for example for builtin Apple apps). + // + // See https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/. + table.TextColumn("team_identifier"), + } +} + +// Generate is called to return the results for the table at query time. +// +// Constraints for generating can be retrieved from the queryContext. +func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + constraints, ok := queryContext.Constraints["path"] + if !ok || len(constraints.Constraints) == 0 { + return nil, errors.New("missing path") + } + + var paths []string + for _, constraint := range constraints.Constraints { + if constraint.Operator != table.OperatorEquals { + return nil, errors.New("only supported operator for 'path' is '='") + } + paths = append(paths, constraint.Expression) + } + + var rows []map[string]string + for _, path := range paths { + row := map[string]string{ + "path": path, + "team_identifier": "", + } + output, err := exec.CommandContext(ctx, "/usr/bin/codesign", + // `codesign --display` does not perform any verification of executables/resources, + // it just parses and displays signature information read from the `Contents` folder. + "--display", + // If we don't set verbose it only prints the executable path. + "--verbose", + path, + ).CombinedOutput() // using CombinedOutput because output is in stderr and stdout is empty. + if err != nil { + // Logging as debug to prevent non signed apps to generate a lot of logged errors. + log.Debug().Err(err).Str("output", string(output)).Str("path", path).Msg("codesign --display failed") + rows = append(rows, row) + continue + } + info := parseCodesignOutput(output) + row["team_identifier"] = info.teamIdentifier + rows = append(rows, row) + } + + return rows, nil +} + +type parsedInfo struct { + teamIdentifier string +} + +func parseCodesignOutput(output []byte) parsedInfo { + const teamIdentifierPrefix = "TeamIdentifier=" + + scanner := bufio.NewScanner(bytes.NewReader(output)) + var info parsedInfo + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, teamIdentifierPrefix) { + info.teamIdentifier = strings.TrimSpace(strings.TrimPrefix(line, teamIdentifierPrefix)) + // "not set" is usually displayed on Apple builtin apps. + if info.teamIdentifier == "not set" { + info.teamIdentifier = "" + } + } + } + return info +} diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go index 18bdc6884f5f..59f0b240077f 100644 --- a/orbit/pkg/table/extension_darwin.go +++ b/orbit/pkg/table/extension_darwin.go @@ -6,6 +6,7 @@ import ( "context" "github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb" + "github.com/fleetdm/fleet/v4/orbit/pkg/table/codesign" "github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info" "github.com/fleetdm/fleet/v4/orbit/pkg/table/dataflattentable" "github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/apfs" @@ -92,6 +93,8 @@ func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) { // Table for parsing Apple Property List files, which are typically stored in ~/Library/Preferences/ dataflattentable.TablePlugin(log.Logger, dataflattentable.PlistType), // table name is "parse_plist" + + table.NewPlugin("codesign", codesign.Columns(), codesign.Generate), } // append platform specific tables diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index b538b1a00295..4624549bf730 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -4171,6 +4171,31 @@ "url": "https://fleetdm.com/tables/cis_audit", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/cis_audit.yml" }, + { + "name": "codesign", + "platforms": [ + "darwin" + ], + "description": "Retrieves codesign information of a given .app path. It doesn't perform (expensive) verification, it just parses the signature from the 'Contents' folder using the \"codesign --display\" command.", + "columns": [ + { + "name": "path", + "type": "text", + "required": true, + "description": "Path is the absolute path to the app folder." + }, + { + "name": "team_identifier", + "type": "text", + "required": false, + "description": "Unique 10-character string generated by Apple that's assigned to a developer account to sign packages. This value is empty on unsigned applications and built-in Apple applications." + } + ], + "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).", + "evented": false, + "url": "https://fleetdm.com/tables/codesign", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/codesign.yml" + }, { "name": "connected_displays", "description": "Provides information about the connected displays of the machine.", diff --git a/schema/tables/codesign.yml b/schema/tables/codesign.yml new file mode 100644 index 000000000000..532da9bc4e8d --- /dev/null +++ b/schema/tables/codesign.yml @@ -0,0 +1,15 @@ +name: codesign +platforms: + - darwin +description: Retrieves codesign information of a given .app path. It doesn't perform (expensive) verification, it just parses the signature from the 'Contents' folder using the "codesign --display" command. +columns: + - name: path + type: text + required: true + description: Path is the absolute path to the app folder. + - name: team_identifier + type: text + required: false + description: Unique 10-character string generated by Apple that's assigned to a developer account to sign packages. This value is empty on unsigned applications and built-in Apple applications. +notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)). +evented: false diff --git a/server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go b/server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go new file mode 100644 index 000000000000..71fa8847e243 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go @@ -0,0 +1,23 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20241110152839, Down_20241110152839) +} + +func Up_20241110152839(tx *sql.Tx) error { + if _, err := tx.Exec(` + ALTER TABLE host_software_installed_paths ADD COLUMN team_identifier VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''`, + ); err != nil { + return fmt.Errorf("failed to add team_identifier to host_software_installed_paths table: %w", err) + } + return nil +} + +func Down_20241110152839(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index cbcc985cb204..7a4b13c3a727 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -587,9 +587,10 @@ CREATE TABLE `host_software_installed_paths` ( `host_id` int unsigned NOT NULL, `software_id` bigint unsigned NOT NULL, `installed_path` text COLLATE utf8mb4_unicode_ci NOT NULL, + `team_identifier` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `host_id_software_id_idx` (`host_id`,`software_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; @@ -1101,9 +1102,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=329 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=330 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241025141856,1,'2020-01-01 01:01:01'),(328,20241030102721,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241025141856,1,'2020-01-01 01:01:01'),(328,20241030102721,1,'2020-01-01 01:01:01'),(329,20241110152839,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index d6cd45f962f8..d89bf398cf37 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -91,7 +91,7 @@ func (ds *Datastore) getHostSoftwareInstalledPaths( error, ) { stmt := ` - SELECT t.id, t.host_id, t.software_id, t.installed_path + SELECT t.id, t.host_id, t.software_id, t.installed_path, t.team_identifier FROM host_software_installed_paths t WHERE t.host_id = ? ` @@ -145,7 +145,10 @@ func hostSoftwareInstalledPathsDelta( continue } - key := fmt.Sprintf("%s%s%s", r.InstalledPath, fleet.SoftwareFieldSeparator, s.ToUniqueStr()) + key := fmt.Sprintf( + "%s%s%s%s%s", + r.InstalledPath, fleet.SoftwareFieldSeparator, r.TeamIdentifier, fleet.SoftwareFieldSeparator, s.ToUniqueStr(), + ) iSPathLookup[key] = r // Anything stored but not reported should be deleted @@ -155,8 +158,8 @@ func hostSoftwareInstalledPathsDelta( } for key := range reported { - parts := strings.SplitN(key, fleet.SoftwareFieldSeparator, 2) - iSPath, unqStr := parts[0], parts[1] + parts := strings.SplitN(key, fleet.SoftwareFieldSeparator, 3) + installedPath, teamIdentifier, unqStr := parts[0], parts[1], parts[2] // Shouldn't be possible ... everything 'reported' should be in the the software table // because this executes after 'ds.UpdateHostSoftware' @@ -172,9 +175,10 @@ func hostSoftwareInstalledPathsDelta( } toInsert = append(toInsert, fleet.HostSoftwareInstalledPath{ - HostID: hostID, - SoftwareID: s.ID, - InstalledPath: iSPath, + HostID: hostID, + SoftwareID: s.ID, + InstalledPath: installedPath, + TeamIdentifier: teamIdentifier, }) } @@ -211,7 +215,7 @@ func insertHostSoftwareInstalledPaths( return nil } - stmt := "INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES %s" + stmt := "INSERT INTO host_software_installed_paths (host_id, software_id, installed_path, team_identifier) VALUES %s" batchSize := 500 for i := 0; i < len(toInsert); i += batchSize { @@ -223,10 +227,10 @@ func insertHostSoftwareInstalledPaths( var args []interface{} for _, v := range batch { - args = append(args, v.HostID, v.SoftwareID, v.InstalledPath) + args = append(args, v.HostID, v.SoftwareID, v.InstalledPath, v.TeamIdentifier) } - placeHolders := strings.TrimSuffix(strings.Repeat("(?, ?, ?), ", len(batch)), ", ") + placeHolders := strings.TrimSuffix(strings.Repeat("(?, ?, ?, ?), ", len(batch)), ", ") stmt := fmt.Sprintf(stmt, placeHolders) _, err := tx.ExecContext(ctx, stmt, args...) @@ -639,7 +643,19 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB( ) // INSERT IGNORE is used to avoid duplicate key errors, which may occur since our previous read came from the replica. stmt := fmt.Sprintf( - "INSERT IGNORE INTO software (name, version, source, `release`, vendor, arch, bundle_identifier, extension_id, browser, title_id, checksum) VALUES %s", + `INSERT IGNORE INTO software ( + name, + version, + source, + `+"`release`"+`, + vendor, + arch, + bundle_identifier, + extension_id, + browser, + title_id, + checksum + ) VALUES %s`, values, ) args := make([]interface{}, 0, totalToProcess*numberOfArgsPerSoftware) @@ -1228,16 +1244,22 @@ func (ds *Datastore) LoadHostSoftware(ctx context.Context, host *fleet.Host, inc return err } - lookup := make(map[uint][]string) + installedPathsList := make(map[uint][]string) + pathSignatureInformation := make(map[uint][]fleet.PathSignatureInformation) for _, ip := range installedPaths { - lookup[ip.SoftwareID] = append(lookup[ip.SoftwareID], ip.InstalledPath) + installedPathsList[ip.SoftwareID] = append(installedPathsList[ip.SoftwareID], ip.InstalledPath) + pathSignatureInformation[ip.SoftwareID] = append(pathSignatureInformation[ip.SoftwareID], fleet.PathSignatureInformation{ + InstalledPath: ip.InstalledPath, + TeamIdentifier: ip.TeamIdentifier, + }) } host.Software = make([]fleet.HostSoftwareEntry, 0, len(software)) for _, s := range software { host.Software = append(host.Software, fleet.HostSoftwareEntry{ - Software: s, - InstalledPaths: lookup[s.ID], + Software: s, + InstalledPaths: installedPathsList[s.ID], + PathSignatureInformation: pathSignatureInformation[s.ID], }) } return nil @@ -1283,7 +1305,7 @@ func (ds *Datastore) AllSoftwareIterator( var args []interface{} stmt := `SELECT - s.id, s.name, s.version, s.source, s.bundle_identifier, s.release, s.arch, s.vendor, s.browser, s.extension_id, s.title_id , + s.id, s.name, s.version, s.source, s.bundle_identifier, s.release, s.arch, s.vendor, s.browser, s.extension_id, s.title_id, COALESCE(sc.cpe, '') AS generated_cpe FROM software s LEFT JOIN software_cpe sc ON (s.id=sc.software_id)` @@ -2524,6 +2546,8 @@ INNER JOIN software_cve scve ON scve.software_id = s.id st.id as software_title_id, s.id as software_id, s.version, + s.bundle_identifier, + s.source, hs.last_opened_at FROM software s @@ -2588,7 +2612,8 @@ INNER JOIN software_cve scve ON scve.software_id = s.id const pathsStmt = ` SELECT hsip.software_id, - hsip.installed_path + hsip.installed_path, + hsip.team_identifier FROM host_software_installed_paths hsip WHERE @@ -2598,8 +2623,9 @@ INNER JOIN software_cve scve ON scve.software_id = s.id software_id, installed_path ` type installedPath struct { - SoftwareID uint `db:"software_id"` - InstalledPath string `db:"installed_path"` + SoftwareID uint `db:"software_id"` + InstalledPath string `db:"installed_path"` + TeamIdentifier string `db:"team_identifier"` } var installedPaths []installedPath stmt, args, err = sqlx.In(pathsStmt, host.ID, softwareIDs) @@ -2614,6 +2640,12 @@ INNER JOIN software_cve scve ON scve.software_id = s.id for _, path := range installedPaths { ver := bySoftwareID[path.SoftwareID] ver.InstalledPaths = append(ver.InstalledPaths, path.InstalledPath) + if ver.Source == "apps" { + ver.SignatureInformation = append(ver.SignatureInformation, fleet.PathSignatureInformation{ + InstalledPath: path.InstalledPath, + TeamIdentifier: path.TeamIdentifier, + }) + } } } } diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index 4e99fe4b6a3b..a613ba82cbe4 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -58,10 +58,10 @@ func TestSoftware(t *testing.T) { {"DeleteSoftwareCPEs", testDeleteSoftwareCPEs}, {"SoftwareByIDNoDuplicatedVulns", testSoftwareByIDNoDuplicatedVulns}, {"SoftwareByIDIncludesCVEPublishedDate", testSoftwareByIDIncludesCVEPublishedDate}, - {"getHostSoftwareInstalledPaths", testGetHostSoftwareInstalledPaths}, - {"hostSoftwareInstalledPathsDelta", testHostSoftwareInstalledPathsDelta}, - {"deleteHostSoftwareInstalledPaths", testDeleteHostSoftwareInstalledPaths}, - {"insertHostSoftwareInstalledPaths", testInsertHostSoftwareInstalledPaths}, + {"GetHostSoftwareInstalledPaths", testGetHostSoftwareInstalledPaths}, + {"HostSoftwareInstalledPathsDelta", testHostSoftwareInstalledPathsDelta}, + {"DeleteHostSoftwareInstalledPaths", testDeleteHostSoftwareInstalledPaths}, + {"InsertHostSoftwareInstalledPaths", testInsertHostSoftwareInstalledPaths}, {"VerifySoftwareChecksum", testVerifySoftwareChecksum}, {"ListHostSoftware", testListHostSoftware}, {"ListIOSHostSoftware", testListIOSHostSoftware}, @@ -1342,7 +1342,7 @@ func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) { // Insert paths for software1 s1Paths := map[string]struct{}{} for _, s := range software1 { - key := fmt.Sprintf("%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, s.ToUniqueStr()) + key := fmt.Sprintf("%s%s%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr()) s1Paths[key] = struct{}{} } require.NoError(t, ds.UpdateHostSoftwareInstalledPaths(context.Background(), host1.ID, s1Paths, mutationResults)) @@ -1353,7 +1353,7 @@ func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) { // Insert paths for software2 s2Paths := map[string]struct{}{} for _, s := range software2 { - key := fmt.Sprintf("%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, s.ToUniqueStr()) + key := fmt.Sprintf("%s%s%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr()) s2Paths[key] = struct{}{} } require.NoError(t, ds.UpdateHostSoftwareInstalledPaths(context.Background(), host2.ID, s2Paths, mutationResults)) @@ -2733,9 +2733,9 @@ func testHostSoftwareInstalledPathsDelta(t *testing.T, ds *Datastore) { t.Run("host has no software but some paths were reported", func(t *testing.T) { reported := make(map[string]struct{}) - reported[fmt.Sprintf("/some/path/%d%s%s", software[0].ID, fleet.SoftwareFieldSeparator, software[0].ToUniqueStr())] = struct{}{} - reported[fmt.Sprintf("/some/path/%d%s%s", software[1].ID+1, fleet.SoftwareFieldSeparator, software[1].ToUniqueStr())] = struct{}{} - reported[fmt.Sprintf("/some/path/%d%s%s", software[2].ID, fleet.SoftwareFieldSeparator, software[2].ToUniqueStr())] = struct{}{} + reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[0].ID, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[0].ToUniqueStr())] = struct{}{} + reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[1].ID+1, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[1].ToUniqueStr())] = struct{}{} + reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[2].ID, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[2].ToUniqueStr())] = struct{}{} var stored []fleet.HostSoftwareInstalledPath _, _, err := hostSoftwareInstalledPathsDelta(host.ID, reported, stored, nil) @@ -2744,7 +2744,7 @@ func testHostSoftwareInstalledPathsDelta(t *testing.T, ds *Datastore) { t.Run("we have some deltas", func(t *testing.T) { getKey := func(s fleet.Software, change uint) string { - return fmt.Sprintf("/some/path/%d%s%s", s.ID+change, fleet.SoftwareFieldSeparator, s.ToUniqueStr()) + return fmt.Sprintf("/some/path/%d%s%s%s%s", s.ID+change, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr()) } reported := make(map[string]struct{}) reported[getKey(software[0], 0)] = struct{}{} @@ -3308,7 +3308,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { installPaths := make([]string, 0, len(software)) for _, s := range software { path := fmt.Sprintf("/some/path/%s", s.Name) - key := fmt.Sprintf("%s%s%s", path, fleet.SoftwareFieldSeparator, s.ToUniqueStr()) + key := fmt.Sprintf("%s%s%s%s%s", path, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr()) swPaths[key] = struct{}{} installPaths = append(installPaths, path) } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index c962d4119c09..2cfaf6deb3ff 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -842,9 +842,12 @@ type Datastore interface { // UpdateHostSoftwareInstalledPaths looks at all software for 'hostID' and based on the contents of // 'reported', either inserts or deletes the corresponding entries in the // 'host_software_installed_paths' table. 'reported' is a set of - // 'software.ToUniqueStr()--installed_path' strings. 'mutationResults' contains the software inventory of + // 'installed_path\0team_identifier\0software.ToUniqueStr()' strings. 'mutationResults' contains the software inventory of // the host (pre-mutations) and the mutations performed after calling 'UpdateHostSoftware', // it is used as DB optimization. + // + // TODO(lucas): We should amend UpdateHostSoftwareInstalledPaths to just accept raw information + // otherwise the caller has to assemble the reported set the same way in all places where it's used. UpdateHostSoftwareInstalledPaths(ctx context.Context, hostID uint, reported map[string]struct{}, mutationResults *UpdateHostSoftwareDBResult) error // UpdateHost updates a host. diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 3ff287c9f10d..e402af547218 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -1185,6 +1185,9 @@ type HostSoftwareInstalledPath struct { SoftwareID uint `db:"software_id"` // InstalledPath is the file system path where the software is installed InstalledPath string `db:"installed_path"` + // TeamIdentifier (not to be confused with Fleet's team IDs) is the Apple's "Team ID" (aka "Developer ID" + // or "Signing ID") of signed applications, see https://developer.apple.com/help/account/manage-your-team/locate-your-team-id. + TeamIdentifier string `db:"team_identifier"` } // HostMacOSProfile represents a macOS profile installed on a host as reported by the macos_profiles diff --git a/server/fleet/software.go b/server/fleet/software.go index 487c1a50e9f6..04b40771312c 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -28,6 +28,10 @@ const ( SoftwareReleaseMaxLength = 64 SoftwareVendorMaxLength = 114 SoftwareArchMaxLength = 16 + + // SoftwareTeamIdentifierMaxLength is the max length for Apple's Team ID, + // see https://developer.apple.com/help/account/manage-your-team/locate-your-team-id + SoftwareTeamIdentifierMaxLength = 10 ) type Vulnerabilities []CVE @@ -271,7 +275,13 @@ type HostSoftwareEntry struct { Software // Where this software was installed on the host, value is derived from the // host_software_installed_paths table. - InstalledPaths []string `json:"installed_paths"` + InstalledPaths []string `json:"installed_paths"` + PathSignatureInformation []PathSignatureInformation `json:"signature_information"` +} + +type PathSignatureInformation struct { + InstalledPath string `json:"installed_path"` + TeamIdentifier string `json:"team_identifier"` } // HostSoftware is the set of software installed on a specific host @@ -383,9 +393,10 @@ func ParseSoftwareLastOpenedAtRowValue(value string) (time.Time, error) { // // All fields are trimmed to fit on Fleet's database. // The vendor field is currently trimmed by removing the extra characters and adding `...` at the end. -func SoftwareFromOsqueryRow(name, version, source, vendor, installedPath, release, arch, bundleIdentifier, extensionId, browser, lastOpenedAt string) ( - *Software, error, -) { +func SoftwareFromOsqueryRow( + name, version, source, vendor, installedPath, release, arch, + bundleIdentifier, extensionId, browser, lastOpenedAt string, +) (*Software, error) { if name == "" { return nil, errors.New("host reported software with empty name") } diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index aae130902a1c..835762c1549e 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -480,15 +480,18 @@ type HostSoftwareUninstall struct { UninstalledAt time.Time `json:"uninstalled_at"` } -// HostSoftwareInstalledVersion represents a version of software installed on a -// host. +// HostSoftwareInstalledVersion represents a version of software installed on a host. type HostSoftwareInstalledVersion struct { - SoftwareID uint `json:"-" db:"software_id"` - SoftwareTitleID uint `json:"-" db:"software_title_id"` - Version string `json:"version" db:"version"` - LastOpenedAt *time.Time `json:"last_opened_at" db:"last_opened_at"` - Vulnerabilities []string `json:"vulnerabilities" db:"vulnerabilities"` - InstalledPaths []string `json:"installed_paths" db:"installed_paths"` + SoftwareID uint `json:"-" db:"software_id"` + SoftwareTitleID uint `json:"-" db:"software_title_id"` + Source string `json:"-" db:"source"` + Version string `json:"version" db:"version"` + BundleIdentifier string `json:"bundle_identifier,omitempty" db:"bundle_identifier"` + LastOpenedAt *time.Time `json:"last_opened_at" db:"last_opened_at"` + + Vulnerabilities []string `json:"vulnerabilities" db:"vulnerabilities"` + InstalledPaths []string `json:"installed_paths"` + SignatureInformation []PathSignatureInformation `json:"signature_information,omitempty"` } // HostSoftwareInstallResultPayload is the payload provided by fleetd to record diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 2e08b4ef9dc3..45c56a3d7642 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -12292,3 +12292,108 @@ func (s *integrationTestSuite) TestHostWithNoPoliciesClearsPolicyCounts() { require.Len(t, listHostsResp.Hosts, 1) require.Equal(t, uint64(0), listHostsResp.Hosts[0].FailingPoliciesCount) } + +func (s *integrationTestSuite) TestHostSoftwareWithTeamIdentifier() { + t := s.T() + ctx := context.Background() + + host, err := s.ds.NewHost(ctx, &fleet.Host{ + NodeKey: ptr.String(t.Name()), + OsqueryHostID: ptr.String(t.Name()), + UUID: t.Name(), + Hostname: t.Name() + "foo.local", + Platform: "darwin", + }) + require.NoError(t, err) + + safariApp := fleet.Software{ + Name: "Safari.app", + BundleIdentifier: "com.apple.safari", + Version: "18.1", + Source: "apps", + } + googleChromeApp := fleet.Software{ + Name: "Google Chrome.app", + BundleIdentifier: "com.google.Chrome", + Version: "130.0.6723.117", + Source: "apps", + } + ghCli := fleet.Software{ + Name: "gh", + Source: "homebrew_packages", + } + + // Update the host's software. + software := []fleet.Software{ + safariApp, googleChromeApp, ghCli, + } + hostSoftware, err := s.ds.UpdateHostSoftware(context.Background(), host.ID, software) + require.NoError(t, err) + require.Len(t, hostSoftware.CurrInstalled(), 3) + + // Update the host's software installed paths for the software above. + // Google Chrome.app will have two installed paths one with team identifier set + // the other one set to empty. + swPaths := map[string]struct{}{} + for _, s := range software { + pathItems := [][2]string{{fmt.Sprintf("/some/path/%s", s.Name), ""}} + if s.Name == "Google Chrome.app" { + pathItems = [][2]string{ + {fmt.Sprintf("/some/path/%s", s.Name), "EQHXZ8M8AV"}, + {fmt.Sprintf("/some/other/path/%s", s.Name), ""}, + } + } + for _, pathItem := range pathItems { + path := pathItem[0] + teamIdentifier := pathItem[1] + key := fmt.Sprintf( + "%s%s%s%s%s", + path, fleet.SoftwareFieldSeparator, teamIdentifier, fleet.SoftwareFieldSeparator, s.ToUniqueStr(), + ) + swPaths[key] = struct{}{} + } + } + err = s.ds.UpdateHostSoftwareInstalledPaths(ctx, host.ID, swPaths, hostSoftware) + require.NoError(t, err) + + hostsCountTs := time.Now().UTC() + err = s.ds.SyncHostsSoftware(context.Background(), hostsCountTs) + require.NoError(t, err) + + getHostSoftwareResp := getHostSoftwareResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), + nil, http.StatusOK, &getHostSoftwareResp, + "per_page", "5", "page", "0", "order_key", "name", "order_direction", "desc", + ) + require.Len(t, getHostSoftwareResp.Software, 3) + require.Equal(t, "Safari.app", getHostSoftwareResp.Software[0].Name) + require.Len(t, getHostSoftwareResp.Software[0].InstalledVersions, 1) + require.Len(t, getHostSoftwareResp.Software[0].InstalledVersions[0].InstalledPaths, 1) + require.Equal(t, "/some/path/Safari.app", getHostSoftwareResp.Software[0].InstalledVersions[0].InstalledPaths[0]) + require.Len(t, getHostSoftwareResp.Software[0].InstalledVersions[0].SignatureInformation, 1) + require.Equal(t, "/some/path/Safari.app", getHostSoftwareResp.Software[0].InstalledVersions[0].SignatureInformation[0].InstalledPath) + require.Empty(t, getHostSoftwareResp.Software[0].InstalledVersions[0].SignatureInformation[0].TeamIdentifier) + + require.Equal(t, "Google Chrome.app", getHostSoftwareResp.Software[1].Name) + require.Len(t, getHostSoftwareResp.Software[1].InstalledVersions, 1) + require.Len(t, getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths, 2) + sort.Slice(getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths, func(i, j int) bool { + return getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[i] < getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[j] + }) + require.Equal(t, "/some/other/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[0]) + require.Equal(t, "/some/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[1]) + require.Len(t, getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation, 2) + sort.Slice(getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation, func(i, j int) bool { + return getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[i].InstalledPath < getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[j].InstalledPath + }) + require.Equal(t, "/some/other/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[0].InstalledPath) + require.Equal(t, "", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[0].TeamIdentifier) + require.Equal(t, "/some/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[1].InstalledPath) + require.Equal(t, "EQHXZ8M8AV", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[1].TeamIdentifier) + + require.Equal(t, "gh", getHostSoftwareResp.Software[2].Name) + require.Len(t, getHostSoftwareResp.Software[2].InstalledVersions, 1) + require.Len(t, getHostSoftwareResp.Software[2].InstalledVersions[0].InstalledPaths, 1) + require.Equal(t, "/some/path/gh", getHostSoftwareResp.Software[2].InstalledVersions[0].InstalledPaths[0]) + require.Nil(t, getHostSoftwareResp.Software[2].InstalledVersions[0].SignatureInformation) +} diff --git a/server/service/osquery.go b/server/service/osquery.go index b2259aeb0858..21555df37270 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -1240,7 +1240,6 @@ func preProcessSoftwareResults( overrides map[string]osquery_utils.DetailQuery, logger log.Logger, ) { - // vsCodeExtensionsExtraQuery := hostDetailQueryPrefix + "software_vscode_extensions" preProcessSoftwareExtraResults(vsCodeExtensionsExtraQuery, host.ID, results, statuses, messages, osquery_utils.DetailQuery{}, logger) @@ -1377,9 +1376,12 @@ func preProcessSoftwareExtraResults( // Do not append results if the main query failed to run. continue } - (*results)[query] = removeOverrides((*results)[query], override) - - (*results)[query] = append((*results)[query], softwareExtraRows...) + if override.SoftwareProcessResults != nil { + (*results)[query] = override.SoftwareProcessResults((*results)[query], softwareExtraRows) + } else { + (*results)[query] = removeOverrides((*results)[query], override) + (*results)[query] = append((*results)[query], softwareExtraRows...) + } return } } diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 286b6b8a28aa..5be2974a12ae 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -1062,6 +1062,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) { hostDetailQueryPrefix + "software_vscode_extensions": {}, hostDetailQueryPrefix + "software_macos_firefox": {}, hostDetailQueryPrefix + "battery": {}, + hostDetailQueryPrefix + "software_macos_codesign": {}, } for name := range queries { require.NotEmpty(t, discovery[name]) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 5797dd969705..da62dfef17a6 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -44,10 +44,13 @@ type DetailQuery struct { // empty, run on all platforms. Platforms []string // SoftwareOverrideMatch is a function that can be used to override a software - // result. The function evaluates a software detail query result row and deletes + // result. The function evaluates a software detail query result row and deletes // the result if the function returns true so the result of this detail query can be // used instead. SoftwareOverrideMatch func(row map[string]string) bool + // SoftwareProcessResults is a function that can be used to process entries of the main + // software query and append or modify data using results of additional queries. + SoftwareProcessResults func(mainSoftwareResults []map[string]string, additionalSoftwareResults []map[string]string) []map[string]string // IngestFunc translates a query result into an update to the host struct, // around data that lives on the hosts table. IngestFunc func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error @@ -820,7 +823,6 @@ var softwareMacOS = DetailQuery{ SELECT name AS name, COALESCE(NULLIF(bundle_short_version, ''), bundle_version) AS version, - 'Application (macOS)' AS type, bundle_identifier AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -833,7 +835,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Python)' AS type, '' AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -846,7 +847,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Chrome)' AS type, '' AS bundle_identifier, identifier AS extension_id, browser_type AS browser, @@ -859,7 +859,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Firefox)' AS type, '' AS bundle_identifier, identifier AS extension_id, 'firefox' AS browser, @@ -872,7 +871,6 @@ UNION SELECT name As name, version AS version, - 'Browser plugin (Safari)' AS type, '' AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -885,7 +883,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Homebrew)' AS type, '' AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -908,7 +905,6 @@ var softwareVSCodeExtensions = DetailQuery{ SELECT name, version, - 'IDE extension (VS Code)' AS type, '' AS bundle_identifier, uuid AS extension_id, '' AS browser, @@ -937,7 +933,6 @@ var softwareLinux = DetailQuery{ SELECT name AS name, version AS version, - 'Package (deb)' AS type, '' AS extension_id, '' AS browser, 'deb_packages' AS source, @@ -951,7 +946,6 @@ UNION SELECT package AS name, version AS version, - 'Package (Portage)' AS type, '' AS extension_id, '' AS browser, 'portage_packages' AS source, @@ -964,7 +958,6 @@ UNION SELECT name AS name, version AS version, - 'Package (RPM)' AS type, '' AS extension_id, '' AS browser, 'rpm_packages' AS source, @@ -977,7 +970,6 @@ UNION SELECT name AS name, version AS version, - 'Package (NPM)' AS type, '' AS extension_id, '' AS browser, 'npm_packages' AS source, @@ -990,7 +982,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Chrome)' AS type, identifier AS extension_id, browser_type AS browser, 'chrome_extensions' AS source, @@ -1003,7 +994,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Firefox)' AS type, identifier AS extension_id, 'firefox' AS browser, 'firefox_addons' AS source, @@ -1016,7 +1006,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Python)' AS type, '' AS extension_id, '' AS browser, 'python_packages' AS source, @@ -1035,7 +1024,6 @@ var softwareWindows = DetailQuery{ SELECT name AS name, version AS version, - 'Program (Windows)' AS type, '' AS extension_id, '' AS browser, 'programs' AS source, @@ -1046,7 +1034,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Python)' AS type, '' AS extension_id, '' AS browser, 'python_packages' AS source, @@ -1057,7 +1044,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (IE)' AS type, '' AS extension_id, '' AS browser, 'ie_extensions' AS source, @@ -1068,7 +1054,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Chrome)' AS type, identifier AS extension_id, browser_type AS browser, 'chrome_extensions' AS source, @@ -1079,7 +1064,6 @@ UNION SELECT name AS name, version AS version, - 'Browser plugin (Firefox)' AS type, identifier AS extension_id, 'firefox' AS browser, 'firefox_addons' AS source, @@ -1090,7 +1074,6 @@ UNION SELECT name AS name, version AS version, - 'Package (Chocolatey)' AS type, '' AS extension_id, '' AS browser, 'chocolatey_packages' AS source, @@ -1108,7 +1091,6 @@ var softwareChrome = DetailQuery{ version AS version, identifier AS extension_id, browser_type AS browser, - 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source, '' AS vendor, '' AS installed_path @@ -1123,10 +1105,13 @@ FROM chrome_extensions`, // Software queries expect specific columns to be present. Reference the // software_{macos|windows|linux} queries for the expected columns. var SoftwareOverrideQueries = map[string]DetailQuery{ - // macos_firefox Differentiates between Firefox and Firefox ESR by checking the RemotingName value in the + // macos_firefox differentiates between Firefox and Firefox ESR by checking the RemotingName value in the // application.ini file. If the RemotingName is 'firefox-esr', the name is set to 'Firefox ESR.app'. + // + // NOTE(lucas): This could be re-written to use SoftwareProcessResults so that this query doesn't need to match + // the columns of the main softwareMacOS query. "macos_firefox": { - Description: "A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd`", + Description: "A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd`", Query: ` WITH app_paths AS ( SELECT path @@ -1145,7 +1130,6 @@ var SoftwareOverrideQueries = map[string]DetailQuery{ ELSE 'Firefox.app' END AS name, COALESCE(NULLIF(apps.bundle_short_version, ''), apps.bundle_version) AS version, - 'Application (macOS)' AS type, apps.bundle_identifier AS bundle_identifier, '' AS extension_id, '' AS browser, @@ -1165,6 +1149,40 @@ var SoftwareOverrideQueries = map[string]DetailQuery{ return row["bundle_identifier"] == "org.mozilla.firefox" }, }, + // macos_codesign collects code signature information of apps on a separate query for two reasons: + // - codesign is a fleetd table (not part of osquery core). + // - Avoid growing the main `software_macos` query + // (having big queries can cause performance issues or be denylisted). + "macos_codesign": { + Query: ` + SELECT a.path, c.team_identifier + FROM apps a + JOIN codesign c ON a.path = c.path + `, + Description: "A software override query[^1] to append codesign information to macOS software entries. Requires `fleetd`", + Platforms: []string{"darwin"}, + Discovery: discoveryTable("codesign"), + SoftwareProcessResults: func(mainSoftwareResults, codesignResults []map[string]string) []map[string]string { + codesignInformation := make(map[string]string) // path -> team_identifier + for _, codesignResult := range codesignResults { + codesignInformation[codesignResult["path"]] = codesignResult["team_identifier"] + } + if len(codesignInformation) == 0 { + return mainSoftwareResults + } + + for _, result := range mainSoftwareResults { + codesignInfo := codesignInformation[result["installed_path"]] + if codesignInfo == "" { + // No codesign information for this application. + continue + } + result["team_identifier"] = codesignInfo + } + + return mainSoftwareResults + }, + }, } var usersQuery = DetailQuery{ @@ -1546,7 +1564,18 @@ func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Ho // NOTE: osquery is sometimes incorrectly returning the value "null" for some install paths. // Thus, we explicitly ignore such value here. strings.ToLower(installedPath) != "null" { - key := fmt.Sprintf("%s%s%s", installedPath, fleet.SoftwareFieldSeparator, s.ToUniqueStr()) + truncateString := func(str string, length int) string { + runes := []rune(str) + if len(runes) > length { + return string(runes[:length]) + } + return str + } + teamIdentifier := truncateString(row["team_identifier"], fleet.SoftwareTeamIdentifierMaxLength) + key := fmt.Sprintf( + "%s%s%s%s%s", + installedPath, fleet.SoftwareFieldSeparator, teamIdentifier, fleet.SoftwareFieldSeparator, s.ToUniqueStr(), + ) sPaths[key] = struct{}{} } } diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 08ee059ac1d8..b15590a574f2 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -307,7 +307,7 @@ func TestGetDetailQueries(t *testing.T) { queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}) qs = baseQueries qs = append(qs, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions", - "software_chrome", "scheduled_query_stats", "software_macos_firefox") + "software_chrome", "scheduled_query_stats", "software_macos_firefox", "software_macos_codesign") require.Len(t, queriesWithUsersAndSoftware, len(qs)) sortedKeysCompare(t, queriesWithUsersAndSoftware, qs) @@ -1338,7 +1338,7 @@ func TestDirectIngestSoftware(t *testing.T) { require.True(t, ds.UpdateHostSoftwareFuncInvoked) require.Len(t, calledWith, 1) - require.Contains(t, strings.Join(maps.Keys(calledWith), " "), fmt.Sprintf("%s%s%s", data[1]["installed_path"], fleet.SoftwareFieldSeparator, data[1]["name"])) + require.Contains(t, strings.Join(maps.Keys(calledWith), " "), fmt.Sprintf("%s%s%s%s%s", data[1]["installed_path"], fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, data[1]["name"])) ds.UpdateHostSoftwareInstalledPathsFuncInvoked = false }) From b805d95244742501861c1ea8a5c736b3232e0df4 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 15 Nov 2024 19:46:28 -0300 Subject: [PATCH 04/36] Cherry pick PR #23855 into RC v4.60.0 (#23866) Cherry pick PR #23855 into RC v4.60.0 --- cmd/osquery-perf/agent.go | 54 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 5c73a3ce89cc..58e237d6913e 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -18,6 +18,7 @@ import ( "net/http" _ "net/http/pprof" "os" + "sort" "strconv" "strings" "sync" @@ -2097,8 +2098,9 @@ func (a *agent) runLiveQuery(query string) (results []map[string]string, status } } -func (a *agent) processQuery(name, query string) ( - handled bool, results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats, +func (a *agent) processQuery(name, query string, cachedResults *cachedResults) ( + handled bool, results []map[string]string, + status *fleet.OsqueryStatus, message *string, stats *fleet.Stats, ) { const ( hostPolicyQueryPrefix = "fleet_policy_query_" @@ -2164,6 +2166,33 @@ func (a *agent) processQuery(name, query string) ( } if ss == fleet.StatusOK { results = a.softwareMacOS() + cachedResults.software = results + } + return true, results, &ss, nil, nil + case name == hostDetailQueryPrefix+"software_macos_codesign": + // Given queries run in lexicographic order software_macos already run and + // cachedResults.software should have its results. + ss := fleet.StatusOK + if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb { + ss = fleet.OsqueryStatus(1) + } + if ss == fleet.StatusOK { + if len(cachedResults.software) > 0 { + for _, s := range cachedResults.software { + if s["source"] != "apps" { + continue + } + installedPath := s["installed_path"] + teamIdentifier := s["name"] // use name to be fixed (more realistic than changing often). + if len(teamIdentifier) > 10 { + teamIdentifier = teamIdentifier[:10] + } + results = append(results, map[string]string{ + "path": installedPath, + "team_identifier": teamIdentifier, + }) + } + } } return true, results, &ss, nil, nil case name == hostDetailQueryPrefix+"software_windows": @@ -2254,6 +2283,10 @@ func (a *agent) processQuery(name, query string) ( } } +type cachedResults struct { + software []map[string]string +} + func (a *agent) DistributedWrite(queries map[string]string) error { r := service.SubmitDistributedQueryResultsRequest{ Results: make(fleet.OsqueryDistributedQueryResults), @@ -2262,8 +2295,21 @@ func (a *agent) DistributedWrite(queries map[string]string) error { Stats: make(map[string]*fleet.Stats), } r.NodeKey = a.nodeKey - for name, query := range queries { - handled, results, status, message, stats := a.processQuery(name, query) + + cachedResults := cachedResults{} + + // Sort queries to be executed by lexicographic name order (for result processing + // to be more predictable). This aligns to how osquery executes the queries. + queryNames := make([]string, 0, len(queries)) + for name := range queries { + queryNames = append(queryNames, name) + } + sort.Strings(queryNames) + + for _, name := range queryNames { + query := queries[name] + + handled, results, status, message, stats := a.processQuery(name, query, &cachedResults) if !handled { // If osquery-perf does not handle the incoming query, // always return status OK and the default query result. From db35c39b965b49bb9cdb8e0e04ec6b68d6268033 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 15 Nov 2024 20:42:22 -0300 Subject: [PATCH 05/36] Cherry pick PR #23857 into RC v4.60.0 (#23867) Cherry pick PR #23857 into RC v4.60.0. --- ...araRulesTable.go => 20241110152840_AddYaraRulesTable.go} | 6 +++--- ....go => 20241110152841_AddAllLabelsToMDMProfileLabels.go} | 6 +++--- ...> 20241110152841_AddAllLabelsToMDMProfileLabels_test.go} | 2 +- server/datastore/mysql/schema.sql | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename server/datastore/mysql/migrations/tables/{20241025141856_AddYaraRulesTable.go => 20241110152840_AddYaraRulesTable.go} (74%) rename server/datastore/mysql/migrations/tables/{20241030102721_AddAllLabelsToMDMProfileLabels.go => 20241110152841_AddAllLabelsToMDMProfileLabels.go} (87%) rename server/datastore/mysql/migrations/tables/{20241030102721_AddAllLabelsToMDMProfileLabels_test.go => 20241110152841_AddAllLabelsToMDMProfileLabels_test.go} (98%) diff --git a/server/datastore/mysql/migrations/tables/20241025141856_AddYaraRulesTable.go b/server/datastore/mysql/migrations/tables/20241110152840_AddYaraRulesTable.go similarity index 74% rename from server/datastore/mysql/migrations/tables/20241025141856_AddYaraRulesTable.go rename to server/datastore/mysql/migrations/tables/20241110152840_AddYaraRulesTable.go index d5f043dc9416..e005c84d679e 100644 --- a/server/datastore/mysql/migrations/tables/20241025141856_AddYaraRulesTable.go +++ b/server/datastore/mysql/migrations/tables/20241110152840_AddYaraRulesTable.go @@ -6,10 +6,10 @@ import ( ) func init() { - MigrationClient.AddMigration(Up_20241016155452, Down_20241016155452) + MigrationClient.AddMigration(Up_20241110152840, Down_20241110152840) } -func Up_20241016155452(tx *sql.Tx) error { +func Up_20241110152840(tx *sql.Tx) error { _, err := tx.Exec(` CREATE TABLE yara_rules ( id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, @@ -24,6 +24,6 @@ CREATE TABLE yara_rules ( return nil } -func Down_20241016155452(tx *sql.Tx) error { +func Down_20241110152840(tx *sql.Tx) error { return nil } diff --git a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels.go similarity index 87% rename from server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go rename to server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels.go index ada5cb8f641a..f95946084d31 100644 --- a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go +++ b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels.go @@ -6,10 +6,10 @@ import ( ) func init() { - MigrationClient.AddMigration(Up_20241030102721, Down_20241030102721) + MigrationClient.AddMigration(Up_20241110152841, Down_20241110152841) } -func Up_20241030102721(tx *sql.Tx) error { +func Up_20241110152841(tx *sql.Tx) error { // Add columns _, err := tx.Exec(`ALTER TABLE mdm_configuration_profile_labels ADD COLUMN require_all BOOL NOT NULL DEFAULT false`) if err != nil { @@ -36,6 +36,6 @@ func Up_20241030102721(tx *sql.Tx) error { return nil } -func Down_20241030102721(tx *sql.Tx) error { +func Down_20241110152841(tx *sql.Tx) error { return nil } diff --git a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels_test.go similarity index 98% rename from server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go rename to server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels_test.go index 4a0a9b97fc00..448d375652bf 100644 --- a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go +++ b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestUp_20241030102721(t *testing.T) { +func TestUp_20241110152841(t *testing.T) { db := applyUpToPrev(t) // insert 2 profiles and 2 declarations diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 7a4b13c3a727..e9740f5e9d2d 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -1104,7 +1104,7 @@ CREATE TABLE `migration_status_tables` ( UNIQUE KEY `id` (`id`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=330 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241025141856,1,'2020-01-01 01:01:01'),(328,20241030102721,1,'2020-01-01 01:01:01'),(329,20241110152839,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( From 4106158c91cf38fb913a4d603f09dbff4e76c688 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:14:30 -0500 Subject: [PATCH 06/36] Cherry pick Download ABM Certificate with correct extension (#23861) (#23869) #23861 --- changes/23021-abm-cert-pem | 1 + .../admin/components/DownloadFileButtons/DownloadABMKey.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/23021-abm-cert-pem diff --git a/changes/23021-abm-cert-pem b/changes/23021-abm-cert-pem new file mode 100644 index 000000000000..c1890e07bb29 --- /dev/null +++ b/changes/23021-abm-cert-pem @@ -0,0 +1 @@ +- Download ABM public key as PEM format instead of CRT diff --git a/frontend/pages/admin/components/DownloadFileButtons/DownloadABMKey.tsx b/frontend/pages/admin/components/DownloadFileButtons/DownloadABMKey.tsx index 891ea45a58fa..7cd342f2da00 100644 --- a/frontend/pages/admin/components/DownloadFileButtons/DownloadABMKey.tsx +++ b/frontend/pages/admin/components/DownloadFileButtons/DownloadABMKey.tsx @@ -21,7 +21,7 @@ interface IDownloadABMKeyProps { } const downloadKeyFile = (data: { public_key: string }) => { - downloadBase64ToFile(data.public_key, "fleet-mdm-apple-bm-public-key.crt"); + downloadBase64ToFile(data.public_key, "fleet-mdm-apple-bm-public-key.pem"); }; // TODO: why can't we use Content-Dispostion for these? We're only getting one file back now. From 0bdd09dd8107c15c84a82ba2f7ce659cacfd9bba Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:19:38 -0500 Subject: [PATCH 07/36] For R.C. - Fleet UI: Hides Never timestamp for empty OS page, clean styling (#23909) --- .../SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx index 56a3aac5f71e..8157684190a8 100644 --- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx @@ -173,13 +173,19 @@ const SoftwareOSTable = ({ router.push(path); }; + // Determines if a user should be able to filter the table + const hasData = data?.os_versions && data?.os_versions.length > 0; + const hasPlatformFilter = platform !== "all"; + + const showFilterHeaders = isSoftwareEnabled && (hasData || hasPlatformFilter); + const renderSoftwareCount = () => { if (!data) return null; return ( <> - {data?.os_versions && data?.counts_updated_at && ( + {showFilterHeaders && data?.counts_updated_at && ( <>} + customControl={showFilterHeaders ? renderPlatformDropdown : undefined} disableNextPage={!data?.meta.has_next_results} searchable={false} onQueryChange={onQueryChange} - stackControls renderCount={renderSoftwareCount} renderTableHelpText={renderTableHelpText} disableMultiRowSelect From 4ee309e23093713528052192eb49bb244c20ff45 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:03:42 -0500 Subject: [PATCH 08/36] Cherry pick Update cloudflare warp uninstall script again (#23874) (#23898) #22773 --- server/mdm/maintainedapps/apps.json | 3 ++- .../testdata/scripts/cloudflare-warp_uninstall.golden.sh | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/mdm/maintainedapps/apps.json b/server/mdm/maintainedapps/apps.json index ce8eaf816e00..f832c158582b 100644 --- a/server/mdm/maintainedapps/apps.json +++ b/server/mdm/maintainedapps/apps.json @@ -30,7 +30,8 @@ { "identifier": "cloudflare-warp", "bundle_identifier": "com.cloudflare.1dot1dot1dot1.macos", - "installer_format": "pkg" + "installer_format": "pkg", + "post_uninstall_scripts": ["/Applications/Cloudflare\\ WARP.app/Contents/Resources/uninstall.sh"] }, { "identifier": "docker", diff --git a/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh index 838490f85ee1..d118bf83e573 100644 --- a/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh +++ b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh @@ -113,6 +113,7 @@ remove_launchctl_service 'com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp' remove_launchctl_service 'com.cloudflare.1dot1dot1dot1.macos.warp.daemon' quit_application 'com.cloudflare.1dot1dot1dot1.macos' sudo pkgutil --forget 'com.cloudflare.1dot1dot1dot1.macos' +/Applications/Cloudflare\ WARP.app/Contents/Resources/uninstall.sh trash $LOGGED_IN_USER '~/Library/Application Scripts/com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp' trash $LOGGED_IN_USER '~/Library/Application Support/com.cloudflare.1dot1dot1dot1.macos' trash $LOGGED_IN_USER '~/Library/Caches/com.cloudflare.1dot1dot1dot1.macos' From 2ca7b286518ea0746f4b55cf3f6d30c47fd14d70 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:04:35 -0500 Subject: [PATCH 09/36] Cherry Pick Scope pending host profile rebuilds (#23772) (#23868) #23772 --- changes/21338-scope-profile-pending-rebuild | 1 + cmd/fleet/serve.go | 1 + server/datastore/mysql/apple_mdm.go | 77 +++++++++++++++---- server/datastore/mysql/mdm.go | 38 +++++---- server/datastore/mysql/microsoft_mdm.go | 46 ++++++++--- server/service/apple_mdm.go | 2 +- .../macos-vm-auto-enroll.sh | 4 + tools/telemetry/README.md | 4 +- 8 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 changes/21338-scope-profile-pending-rebuild diff --git a/changes/21338-scope-profile-pending-rebuild b/changes/21338-scope-profile-pending-rebuild new file mode 100644 index 000000000000..59e48839557e --- /dev/null +++ b/changes/21338-scope-profile-pending-rebuild @@ -0,0 +1 @@ +- Speed up adding and removing profiles to large teams by an order of magnitude diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 4c836bebdccd..4f93d5a540cc 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -189,6 +189,7 @@ the way that the Fleet server works. if config.MysqlReadReplica.Address != "" { opts = append(opts, mysql.Replica(&config.MysqlReadReplica)) } + // NOTE this will disable OTEL/APM interceptor if dev && os.Getenv("FLEET_DEV_ENABLE_SQL_INTERCEPTOR") != "" { opts = append(opts, mysql.WithInterceptor(&devSQLInterceptor{ logger: kitlog.With(logger, "component", "sql-interceptor"), diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 5188e3e9b139..9c8e522988a5 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -1940,12 +1940,16 @@ func (ds *Datastore) bulkDeleteMDMAppleHostsConfigProfilesDB(ctx context.Context return nil } +// NOTE If onlyProfileUUIDs is provided (not nil), only profiles with +// those UUIDs will be update instead of rebuilding all pending +// profiles for hosts func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( ctx context.Context, tx sqlx.ExtContext, - uuids []string, + hostUUIDs []string, + onlyProfileUUIDs []string, ) (updatedDB bool, err error) { - if len(uuids) == 0 { + if len(hostUUIDs) == 0 { return false, nil } @@ -1972,6 +1976,11 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( // considers "remove" operations that have NULL status, which it would // update to make its status to NULL). + profileHostIn := "h.uuid IN (?)" + if len(onlyProfileUUIDs) > 0 { + profileHostIn = "mae.profile_uuid IN (?) AND " + profileHostIn + } + toInstallStmt := fmt.Sprintf(` SELECT ds.profile_uuid as profile_uuid, @@ -1990,7 +1999,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( ( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR -- profiles in A and B but with operation type "remove" ( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) ) -`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)")) +`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, profileHostIn, profileHostIn, profileHostIn, profileHostIn)) // batches of 10K hosts because h.uuid appears three times in the // query, and the max number of prepared statements is 65K, this was @@ -2000,19 +2009,35 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( if ds.testSelectMDMProfilesBatchSize > 0 { selectProfilesBatchSize = ds.testSelectMDMProfilesBatchSize } - selectProfilesTotalBatches := int(math.Ceil(float64(len(uuids)) / float64(selectProfilesBatchSize))) + selectProfilesTotalBatches := int(math.Ceil(float64(len(hostUUIDs)) / float64(selectProfilesBatchSize))) var wantedProfiles []*fleet.MDMAppleProfilePayload for i := 0; i < selectProfilesTotalBatches; i++ { start := i * selectProfilesBatchSize end := start + selectProfilesBatchSize - if end > len(uuids) { - end = len(uuids) + if end > len(hostUUIDs) { + end = len(hostUUIDs) } - batchUUIDs := uuids[start:end] + batchUUIDs := hostUUIDs[start:end] + + var stmt string + var args []any + var err error + + if len(onlyProfileUUIDs) > 0 { + stmt, args, err = sqlx.In( + toInstallStmt, + onlyProfileUUIDs, batchUUIDs, + onlyProfileUUIDs, batchUUIDs, + onlyProfileUUIDs, batchUUIDs, + onlyProfileUUIDs, batchUUIDs, + fleet.MDMOperationTypeRemove, + ) + } else { + stmt, args, err = sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) + } - stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) if err != nil { return false, ctxerr.Wrapf(ctx, err, "building statement to select profiles to install, batch %d of %d", i, selectProfilesTotalBatches) @@ -2030,6 +2055,11 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( // Exclude macOS only profiles from iPhones/iPads. wantedProfiles = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(wantedProfiles) + narrowByProfiles := "" + if len(onlyProfileUUIDs) > 0 { + narrowByProfiles = "AND hmap.profile_uuid IN (?)" + } + toRemoveStmt := fmt.Sprintf(` SELECT hmap.profile_uuid as profile_uuid, @@ -2045,7 +2075,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( RIGHT JOIN host_mdm_apple_profiles hmap ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid WHERE - hmap.host_uuid IN (?) AND + hmap.host_uuid IN (?) %s AND -- profiles that are in B but not in A ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND -- except "remove" operations in any state @@ -2059,19 +2089,36 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( mcpl.apple_profile_uuid = hmap.profile_uuid AND mcpl.label_id IS NULL ) -`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)")) +`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, profileHostIn, profileHostIn, profileHostIn, profileHostIn), narrowByProfiles) var currentProfiles []*fleet.MDMAppleProfilePayload for i := 0; i < selectProfilesTotalBatches; i++ { start := i * selectProfilesBatchSize end := start + selectProfilesBatchSize - if end > len(uuids) { - end = len(uuids) + if end > len(hostUUIDs) { + end = len(hostUUIDs) } - batchUUIDs := uuids[start:end] + batchUUIDs := hostUUIDs[start:end] + + var stmt string + var args []any + var err error + + if len(onlyProfileUUIDs) > 0 { + stmt, args, err = sqlx.In( + toRemoveStmt, + onlyProfileUUIDs, batchUUIDs, + onlyProfileUUIDs, batchUUIDs, + onlyProfileUUIDs, batchUUIDs, + onlyProfileUUIDs, batchUUIDs, + batchUUIDs, onlyProfileUUIDs, + fleet.MDMOperationTypeRemove, + ) + } else { + stmt, args, err = sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) + } - stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove) if err != nil { return false, ctxerr.Wrap(ctx, err, "building profiles to remove statement") } @@ -2431,7 +2478,7 @@ func generateDesiredStateQuery(entityType string) string { COUNT(*) as ${countEntityLabelsColumn}, COUNT(mel.label_id) as count_non_broken_labels, COUNT(lm.label_id) as count_host_labels, - -- this helps avoid the case where the host is not a member of a label + -- this helps avoid the case where the host is not a member of a label -- just because it hasn't reported results for that label yet. SUM(CASE WHEN lbl.created_at IS NOT NULL AND h.label_updated_at >= lbl.created_at THEN 1 ELSE 0 END) as count_host_updated_after_labels FROM diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index 66f90c06f6af..5943b7ba8a4e 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -461,28 +461,36 @@ func (ds *Datastore) bulkSetPendingMDMHostProfilesDB( } case len(macProfUUIDs) > 0: - // TODO: if a very large number (~65K) of profile UUIDs was provided, could + // TODO: if a very large number (~65K/2) of profile UUIDs was provided, could // result in too many placeholders (not an immediate concern). uuidStmt = ` SELECT DISTINCT h.uuid, h.platform FROM hosts h JOIN mdm_apple_configuration_profiles macp ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0) +LEFT JOIN host_mdm_apple_profiles hmap + ON h.uuid = hmap.host_uuid WHERE - macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')` - args = append(args, macProfUUIDs) + macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') +OR + hmap.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')` + args = append(args, macProfUUIDs, macProfUUIDs) case len(winProfUUIDs) > 0: - // TODO: if a very large number (~65K) of profile IDs was provided, could + // TODO: if a very large number (~65K/2) of profile IDs was provided, could // result in too many placeholders (not an immediate concern). uuidStmt = ` SELECT DISTINCT h.uuid, h.platform FROM hosts h JOIN mdm_windows_configuration_profiles mawp ON h.team_id = mawp.team_id OR (h.team_id IS NULL AND mawp.team_id = 0) +LEFT JOIN host_mdm_windows_profiles hmwp + ON h.uuid = hmwp.host_uuid WHERE - mawp.profile_uuid IN (?) AND h.platform = 'windows'` - args = append(args, winProfUUIDs) + mawp.profile_uuid IN (?) AND h.platform = 'windows' +OR + hmwp.profile_uuid IN (?) AND h.platform = 'windows'` + args = append(args, winProfUUIDs, winProfUUIDs) } @@ -515,12 +523,12 @@ WHERE } } - updates.AppleConfigProfile, err = ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts) + updates.AppleConfigProfile, err = ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts, profileUUIDs) if err != nil { return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles") } - updates.WindowsConfigProfile, err = ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts) + updates.WindowsConfigProfile, err = ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts, profileUUIDs) if err != nil { return updates, ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles") } @@ -834,7 +842,7 @@ FROM GROUP BY checksum ) cs ON macp.checksum = cs.checksum WHERE - macp.team_id = ? AND + macp.team_id = ? AND NOT EXISTS ( SELECT 1 @@ -865,16 +873,16 @@ FROM mdm_apple_configuration_profiles GROUP BY checksum ) cs ON macp.checksum = cs.checksum - JOIN mdm_configuration_profile_labels mcpl + JOIN mdm_configuration_profile_labels mcpl ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 0 - LEFT OUTER JOIN label_membership lm + LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id AND lm.host_id = ? WHERE macp.team_id = ? GROUP BY identifier HAVING - count_profile_labels > 0 AND + count_profile_labels > 0 AND count_host_labels = count_profile_labels UNION @@ -897,9 +905,9 @@ FROM mdm_apple_configuration_profiles GROUP BY checksum ) cs ON macp.checksum = cs.checksum - JOIN mdm_configuration_profile_labels mcpl + JOIN mdm_configuration_profile_labels mcpl ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 1 - LEFT OUTER JOIN label_membership lm + LEFT OUTER JOIN label_membership lm ON lm.label_id = mcpl.label_id AND lm.host_id = ? WHERE macp.team_id = ? @@ -907,7 +915,7 @@ GROUP BY identifier HAVING -- considers only the profiles with labels, without any broken label, and with the host not in any label - count_profile_labels > 0 AND + count_profile_labels > 0 AND count_profile_labels = count_non_broken_labels AND count_host_labels = 0 ` diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 524888cf8d23..9e773afddb06 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -1267,7 +1267,7 @@ func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fl // be without and use the reader replica? err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { var err error - result, err = listMDMWindowsProfilesToInstallDB(ctx, tx, nil) + result, err = listMDMWindowsProfilesToInstallDB(ctx, tx, nil, nil) return err }) return result, err @@ -1277,6 +1277,7 @@ func listMDMWindowsProfilesToInstallDB( ctx context.Context, tx sqlx.ExtContext, hostUUIDs []string, + onlyProfileUUIDs []string, ) ([]*fleet.MDMWindowsProfilePayload, error) { // The query below is a set difference between: // @@ -1318,14 +1319,29 @@ func listMDMWindowsProfilesToInstallDB( hostFilter := "TRUE" if len(hostUUIDs) > 0 { - hostFilter = "h.uuid IN (?)" + if len(onlyProfileUUIDs) > 0 { + hostFilter = "mwcp.profile_uuid IN (?) AND h.uuid IN (?)" + } else { + hostFilter = "h.uuid IN (?)" + } } var err error args := []any{fleet.MDMOperationTypeInstall} query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter, hostFilter) if len(hostUUIDs) > 0 { - query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall) + if len(onlyProfileUUIDs) > 0 { + query, args, err = sqlx.In( + query, + onlyProfileUUIDs, hostUUIDs, + onlyProfileUUIDs, hostUUIDs, + onlyProfileUUIDs, hostUUIDs, + onlyProfileUUIDs, hostUUIDs, + fleet.MDMOperationTypeInstall, + ) + } else { + query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall) + } if err != nil { return nil, ctxerr.Wrap(ctx, err, "building sqlx.In") } @@ -1340,7 +1356,7 @@ func (ds *Datastore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fle var result []*fleet.MDMWindowsProfilePayload err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { var err error - result, err = listMDMWindowsProfilesToRemoveDB(ctx, tx, nil) + result, err = listMDMWindowsProfilesToRemoveDB(ctx, tx, nil, nil) return err }) @@ -1351,6 +1367,7 @@ func listMDMWindowsProfilesToRemoveDB( ctx context.Context, tx sqlx.ExtContext, hostUUIDs []string, + onlyProfileUUIDs []string, ) ([]*fleet.MDMWindowsProfilePayload, error) { // The query below is a set difference between: // @@ -1374,7 +1391,11 @@ func listMDMWindowsProfilesToRemoveDB( hostFilter := "TRUE" if len(hostUUIDs) > 0 { - hostFilter = "hmwp.host_uuid IN (?)" + if len(onlyProfileUUIDs) > 0 { + hostFilter = "hmwp.profile_uuid IN (?) AND hmwp.host_uuid IN (?)" + } else { + hostFilter = "hmwp.host_uuid IN (?)" + } } query := fmt.Sprintf(` @@ -1408,7 +1429,11 @@ func listMDMWindowsProfilesToRemoveDB( var err error var args []any if len(hostUUIDs) > 0 { - query, args, err = sqlx.In(query, hostUUIDs) + if len(onlyProfileUUIDs) > 0 { + query, args, err = sqlx.In(query, onlyProfileUUIDs, hostUUIDs) + } else { + query, args, err = sqlx.In(query, hostUUIDs) + } if err != nil { return nil, err } @@ -1917,18 +1942,19 @@ ON DUPLICATE KEY UPDATE func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB( ctx context.Context, tx sqlx.ExtContext, - uuids []string, + hostUUIDs []string, + onlyProfileUUIDs []string, ) (updatedDB bool, err error) { - if len(uuids) == 0 { + if len(hostUUIDs) == 0 { return false, nil } - profilesToInstall, err := listMDMWindowsProfilesToInstallDB(ctx, tx, uuids) + profilesToInstall, err := listMDMWindowsProfilesToInstallDB(ctx, tx, hostUUIDs, onlyProfileUUIDs) if err != nil { return false, ctxerr.Wrap(ctx, err, "list profiles to install") } - profilesToRemove, err := listMDMWindowsProfilesToRemoveDB(ctx, tx, uuids) + profilesToRemove, err := listMDMWindowsProfilesToRemoveDB(ctx, tx, hostUUIDs, onlyProfileUUIDs) if err != nil { return false, ctxerr.Wrap(ctx, err, "list profiles to remove") } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index aa6fcaf05a7a..0e0255a09799 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -847,7 +847,7 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID } // cannot use the profile ID as it is now deleted - if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil { + if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{profileUUID}, nil); err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending host profiles") } diff --git a/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh b/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh index bc11878a9377..dbafc8bc01cc 100755 --- a/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh +++ b/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh @@ -65,6 +65,10 @@ echo "Deleting old fleet package" echo "Creating fleet package..." ./build/fleetctl package --type=pkg --enable-scripts --fleet-desktop --disable-open-folder --fleet-url="$FLEET_URL" --enroll-secret="$FLEET_ENROLL_SECRET" +if [ ! -f fleet-osquery.pkg ]; then + echo "package not generated" + exit 1 +fi if tart list | grep $vm_name >/dev/null 2>&1; then echo 'Enrollment test VM exists, deleting...' diff --git a/tools/telemetry/README.md b/tools/telemetry/README.md index fda2e3790dde..940c8f267c70 100644 --- a/tools/telemetry/README.md +++ b/tools/telemetry/README.md @@ -17,6 +17,6 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \ --logging_tracing_enabled=true \ --logging_tracing_type=opentelemetry \ --dev --logging_debug -``` +``` -Afterward, you can navigate to http://localhost:16686/ to access the Jaeger UI. +Afterwards, you can navigate to http://localhost:16686/ to access the Jaeger UI. From 56fbac9f4b562be60605353719e1372866d36f8e Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 18 Nov 2024 16:22:41 -0600 Subject: [PATCH 10/36] Added activity item for fleetd enrollment with host serial and display name. (#23790) (#23926) #22810 (cherry picked from commit 698e9e80fe6971d63c542718436997092fb96d22) --- changes/22810-fleetd-enroll-activity | 1 + cmd/osquery-perf/agent.go | 1 + docs/Contributing/Audit-logs.md | 17 +++++++++++ frontend/interfaces/activity.ts | 1 + .../ActivityItem/ActivityItem.tsx | 11 +++++++ orbit/changes/22810-fleetd-enroll-activity | 2 ++ orbit/cmd/orbit/orbit.go | 26 ++++++++++++----- server/datastore/mysql/hosts.go | 29 +++++++++++++++---- server/datastore/mysql/hosts_test.go | 28 ++++++++++++++++-- server/fleet/activities.go | 20 +++++++++++++ server/fleet/orbit.go | 4 +++ server/service/integration_mdm_test.go | 14 +++++++++ server/service/orbit.go | 21 +++++++++++++- server/service/orbit_client.go | 2 ++ 14 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 changes/22810-fleetd-enroll-activity create mode 100644 orbit/changes/22810-fleetd-enroll-activity diff --git a/changes/22810-fleetd-enroll-activity b/changes/22810-fleetd-enroll-activity new file mode 100644 index 000000000000..b9b9380a05df --- /dev/null +++ b/changes/22810-fleetd-enroll-activity @@ -0,0 +1 @@ +Added activity item for fleetd enrollment with host serial and display name. diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 58e237d6913e..6b306da0128f 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -1413,6 +1413,7 @@ func (a *agent) orbitEnroll() error { EnrollSecret: a.EnrollSecret, HardwareUUID: a.UUID, HardwareSerial: a.SerialNumber, + Hostname: a.CachedString("hostname"), } jsonBytes, err := json.Marshal(params) if err != nil { diff --git a/docs/Contributing/Audit-logs.md b/docs/Contributing/Audit-logs.md index 85f22fe2192f..26c8a02f3dc5 100644 --- a/docs/Contributing/Audit-logs.md +++ b/docs/Contributing/Audit-logs.md @@ -521,6 +521,23 @@ This activity contains the following fields: } ``` +## fleet_enrolled + +Generated when a host is enrolled to Fleet (Fleet's agent fleetd is installed). + +This activity contains the following fields: +- "host_serial": Serial number of the host. +- "host_display_name": Display name of the host. + +#### Example + +```json +{ + "host_serial": "B04FL3ALPT21", + "host_display_name": "WIN-DESKTOP-JGS78KJ7C" +} +``` + ## mdm_enrolled Generated when a host is enrolled in Fleet's MDM. diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 0d7b3acb28ea..d6fcafc8c7ce 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -33,6 +33,7 @@ export enum ActivityType { UserDeletedGlobalRole = "deleted_user_global_role", UserChangedTeamRole = "changed_user_team_role", UserDeletedTeamRole = "deleted_user_team_role", + FleetEnrolled = "fleet_enrolled", MdmEnrolled = "mdm_enrolled", MdmUnenrolled = "mdm_unenrolled", EditedMacosMinVersion = "edited_macos_min_version", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index 245f7aaddde7..6318b9ce8957 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -279,6 +279,14 @@ const TAGGED_TEMPLATES = { ); }, + fleetEnrolled: (activity: IActivity) => { + const hostDisplayName = activity.details?.host_display_name ? ( + {activity.details.host_display_name} + ) : ( + "A host" + ); + return <>{hostDisplayName} enrolled in Fleet.; + }, mdmEnrolled: (activity: IActivity) => { if (activity.details?.mdm_platform === "microsoft") { return ( @@ -1167,6 +1175,9 @@ const getDetail = ( case ActivityType.UserDeletedTeamRole: { return TAGGED_TEMPLATES.userDeletedTeamRole(activity); } + case ActivityType.FleetEnrolled: { + return TAGGED_TEMPLATES.fleetEnrolled(activity); + } case ActivityType.MdmEnrolled: { return TAGGED_TEMPLATES.mdmEnrolled(activity); } diff --git a/orbit/changes/22810-fleetd-enroll-activity b/orbit/changes/22810-fleetd-enroll-activity new file mode 100644 index 000000000000..2b99a1a8608e --- /dev/null +++ b/orbit/changes/22810-fleetd-enroll-activity @@ -0,0 +1,2 @@ +Added computer_name and hardware_model for fleetd enrollment. +Added serial number for fleetd enrollment for Windows hosts (already present for macOS and Linux). diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index da467570bad9..cf77cd197199 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -694,6 +694,8 @@ func main() { HardwareUUID: osqueryHostInfo.HardwareUUID, Hostname: osqueryHostInfo.Hostname, Platform: osqueryHostInfo.Platform, + ComputerName: osqueryHostInfo.ComputerName, + HardwareModel: osqueryHostInfo.HardwareModel, } if runtime.GOOS == "darwin" { @@ -737,13 +739,6 @@ func main() { orbitHostInfo.OsqueryIdentifier = osqueryHostInfo.InstanceID } - // The hardware serial was not sent when Windows MDM was implemented, - // thus we clear its value here to not break any existing enroll functionality - // on the server. - if runtime.GOOS == "windows" { - orbitHostInfo.HardwareSerial = "" - } - var ( options []osquery.Option // optionsAfterFlagfile is populated with options that will be set after the '--flagfile' argument @@ -1697,6 +1692,10 @@ type osqueryHostInfo struct { HardwareSerial string `json:"hardware_serial"` // Hostname is the device's hostname (extracted from `system_info` osquery table). Hostname string `json:"hostname"` + // ComputerName is the friendly computer name (optional) (extracted from `system_info` osquery table). + ComputerName string `json:"computer_name"` + // HardwareModel is the device's hardware model (extracted from `system_info` osquery table). + HardwareModel string `json:"hardware_model"` // Platform is the device's platform as defined by osquery (extracted from `os_version` osquery table). Platform string `json:"platform"` // InstanceID is the osquery's randomly generated instance ID @@ -1714,7 +1713,18 @@ func getHostInfo(osqueryPath string, osqueryDBPath string) (*osqueryHostInfo, er if err := os.MkdirAll(filepath.Dir(osqueryDBPath), constant.DefaultDirMode); err != nil { return nil, err } - const systemQuery = "SELECT si.uuid, si.hardware_serial, si.hostname, os.platform, os.version as os_version, oi.instance_id, oi.version as osquery_version FROM system_info si, os_version os, osquery_info oi" + const systemQuery = ` + SELECT + si.uuid, + si.hardware_serial, + si.hostname, + si.computer_name, + si.hardware_model, + os.platform, + os.version as os_version, + oi.instance_id, + oi.version as osquery_version + FROM system_info si, os_version os, osquery_info oi` args := []string{ "-S", "--database_path", osqueryDBPath, diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index d259e914098a..ee189ebef478 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1904,9 +1904,20 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf } // NOTE: allow an empty serial, currently it is empty for Windows. - var host fleet.Host + host := fleet.Host{ + ComputerName: hostInfo.ComputerName, + Hostname: hostInfo.Hostname, + HardwareModel: hostInfo.HardwareModel, + HardwareSerial: hostInfo.HardwareSerial, + } err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { - enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier, hostInfo.HardwareUUID, hostInfo.HardwareSerial) + serialToMatch := hostInfo.HardwareSerial + if hostInfo.Platform == "windows" { + // For Windows, don't match by serial number to retain legacy functionality. + serialToMatch = "" + } + enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier, + hostInfo.HardwareUUID, serialToMatch) // If the osquery identifier that osqueryd will use was not sent by Orbit, then use the hardware UUID as identifier // (using the hardware UUID is Orbit's default behavior). @@ -1936,6 +1947,8 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf uuid = COALESCE(NULLIF(uuid, ''), ?), osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?), hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?), + computer_name = COALESCE(NULLIF(computer_name, ''), ?), + hardware_model = COALESCE(NULLIF(hardware_model, ''), ?), team_id = ? WHERE id = ?` _, err := tx.ExecContext(ctx, sqlUpdate, @@ -1943,6 +1956,8 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf hostInfo.HardwareUUID, osqueryIdentifier, hostInfo.HardwareSerial, + hostInfo.ComputerName, + hostInfo.HardwareModel, teamID, enrolledHostInfo.ID, ) @@ -1977,8 +1992,10 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf orbit_node_key, hardware_serial, hostname, + computer_name, + hardware_model, platform - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?) ` result, err := tx.ExecContext(ctx, sqlInsert, zeroTime, @@ -1992,6 +2009,8 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf orbitNodeKey, hostInfo.HardwareSerial, hostInfo.Hostname, + hostInfo.ComputerName, + hostInfo.HardwareModel, hostInfo.Platform, ) if err != nil { @@ -1999,9 +2018,9 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf } hostID, _ := result.LastInsertId() const sqlHostDisplayName = ` - INSERT INTO host_display_names (host_id, display_name) VALUES (?, '') + INSERT INTO host_display_names (host_id, display_name) VALUES (?, ?) ` - _, err = tx.ExecContext(ctx, sqlHostDisplayName, hostID) + _, err = tx.ExecContext(ctx, sqlHostDisplayName, hostID, host.DisplayName()) if err != nil { return ctxerr.Wrap(ctx, err, "insert host_display_names") } diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index fd41dc45e860..4e0269ad3588 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -8061,6 +8061,11 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { func testHostsEnrollOrbit(t *testing.T, ds *Datastore) { ctx := context.Background() + const ( + computerName = "My computer" + hardwareModel = "CMP-1000" + ) + createHost := func(osqueryID, serial string) *fleet.Host { dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) var osqueryIDPtr *string @@ -8075,6 +8080,8 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) { DetailUpdatedAt: dbZeroTime, OsqueryHostID: osqueryIDPtr, RefetchRequested: true, + ComputerName: computerName, + HardwareModel: hardwareModel, }) require.NoError(t, err) return h @@ -8112,10 +8119,19 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) { h, err = ds.EnrollOrbit(ctx, true, fleet.OrbitHostInfo{ HardwareUUID: *hBoth.OsqueryHostID, HardwareSerial: hBoth.HardwareSerial, + ComputerName: hBoth.ComputerName, + HardwareModel: hBoth.HardwareModel, }, uuid.New().String(), nil) require.NoError(t, err) require.Equal(t, hBoth.ID, h.ID) - require.Empty(t, h.HardwareSerial) // this is just to prove that it was loaded based on osquery_host_id, the serial was not set in the lookup + assert.Equal(t, hBoth.HardwareSerial, h.HardwareSerial) + assert.Equal(t, hBoth.ComputerName, h.ComputerName) + assert.Equal(t, hBoth.HardwareModel, h.HardwareModel) + h, err = ds.Host(ctx, h.ID) + require.NoError(t, err) + assert.Equal(t, hBoth.HardwareSerial, h.HardwareSerial) + assert.Equal(t, hBoth.ComputerName, h.ComputerName) + assert.Equal(t, hBoth.HardwareModel, h.HardwareModel) // enroll with osquery id from hBoth and serial from hSerialNoOsquery (should // use the osquery match) @@ -8125,14 +8141,17 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) { }, uuid.New().String(), nil) require.NoError(t, err) require.Equal(t, hBoth.ID, h.ID) - require.Empty(t, h.HardwareSerial) + assert.Equal(t, hSerialNoOsquery.HardwareSerial, h.HardwareSerial) // enroll with no match, will create a new one + newSerial := uuid.NewString() h, err = ds.EnrollOrbit(ctx, true, fleet.OrbitHostInfo{ HardwareUUID: uuid.New().String(), - HardwareSerial: uuid.New().String(), + HardwareSerial: newSerial, Hostname: "foo2", Platform: "darwin", + ComputerName: "New computer", + HardwareModel: "ABC-3000", }, uuid.New().String(), nil) require.NoError(t, err) require.Greater(t, h.ID, hBoth.ID) @@ -8141,6 +8160,9 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Equal(t, "foo2", h.Hostname) require.Equal(t, "darwin", h.Platform) + assert.Equal(t, "New computer", h.ComputerName) + assert.Equal(t, "ABC-3000", h.HardwareModel) + assert.Equal(t, newSerial, h.HardwareSerial) // simulate a "corrupt database" where two hosts have the same serial and // enroll by serial should always use the same (the smaller ID) diff --git a/server/fleet/activities.go b/server/fleet/activities.go index f62689adaaa8..751218ac6eb5 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -50,6 +50,7 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeChangedUserTeamRole{}, ActivityTypeDeletedUserTeamRole{}, + ActivityTypeFleetEnrolled{}, ActivityTypeMDMEnrolled{}, ActivityTypeMDMUnenrolled{}, @@ -795,6 +796,25 @@ func (a ActivityTypeDeletedUserTeamRole) Documentation() (activity string, detai }` } +type ActivityTypeFleetEnrolled struct { + HostSerial string `json:"host_serial"` + HostDisplayName string `json:"host_display_name"` +} + +func (a ActivityTypeFleetEnrolled) ActivityName() string { + return "fleet_enrolled" +} + +func (a ActivityTypeFleetEnrolled) Documentation() (activity string, details string, detailsExample string) { + return `Generated when a host is enrolled to Fleet (Fleet's agent fleetd is installed).`, + `This activity contains the following fields: +- "host_serial": Serial number of the host. +- "host_display_name": Display name of the host.`, `{ + "host_serial": "B04FL3ALPT21", + "host_display_name": "WIN-DESKTOP-JGS78KJ7C" +}` +} + type ActivityTypeMDMEnrolled struct { HostSerial string `json:"host_serial"` HostDisplayName string `json:"host_display_name"` diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index 1d340b1e7b34..e3c0c7b0db3e 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -91,6 +91,10 @@ type OrbitHostInfo struct { // // If not set, then the HardwareUUID is used/set as the osquery identifier. OsqueryIdentifier string + // ComputerName is the device's friendly name (optional). + ComputerName string + // HardwareModel is the device's hardware model. For example: Standard PC (Q35 + ICH9, 2009) + HardwareModel string } // ExtensionInfo holds the data of a osquery extension to apply to an Orbit client. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 2d985423286a..ddd1f5822b8e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -2661,10 +2661,14 @@ func (s *integrationMDMTestSuite) TestEnrollOrbitAfterDEPSync() { // enroll the host from orbit, it should match the host above via the serial var resp EnrollOrbitResponse hostUUID := uuid.New().String() + h.ComputerName = "My Mac" + h.HardwareModel = "MacBook Pro" s.DoJSON("POST", "/api/fleet/orbit/enroll", EnrollOrbitRequest{ EnrollSecret: secret, HardwareUUID: hostUUID, // will not match any existing host HardwareSerial: h.HardwareSerial, + ComputerName: h.ComputerName, + HardwareModel: h.HardwareModel, }, http.StatusOK, &resp) require.NotEmpty(t, resp.OrbitNodeKey) @@ -2674,11 +2678,21 @@ func (s *integrationMDMTestSuite) TestEnrollOrbitAfterDEPSync() { s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp) require.Equal(t, h.ID, hostResp.Host.ID) require.NotEqual(t, dbZeroTime, hostResp.Host.LastEnrolledAt) + assert.Equal(t, h.ComputerName, hostResp.Host.ComputerName) + assert.Equal(t, h.HardwareModel, hostResp.Host.HardwareModel) + assert.Equal(t, h.HardwareSerial, hostResp.Host.HardwareSerial) + assert.Equal(t, h.DisplayName(), hostResp.Host.DisplayName) got, err := s.ds.LoadHostByOrbitNodeKey(ctx, resp.OrbitNodeKey) require.NoError(t, err) require.Equal(t, h.ID, got.ID) + s.lastActivityMatches( + "fleet_enrolled", + fmt.Sprintf(`{"host_display_name": "%s", "host_serial": "%s"}`, h.DisplayName(), h.HardwareSerial), + 0, + ) + // enroll the host from osquery, it should match the same host var osqueryResp enrollAgentResponse osqueryID := uuid.New().String() diff --git a/server/service/orbit.go b/server/service/orbit.go index e5b73e3e5275..be10578dc5e6 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -41,6 +41,10 @@ type EnrollOrbitRequest struct { // OsqueryIdentifier holds the identifier used by osquery. // If not set, then the hardware UUID is used to match orbit and osquery. OsqueryIdentifier string `json:"osquery_identifier"` + // ComputerName is the device's friendly name (optional). + ComputerName string `json:"computer_name"` + // HardwareModel is the device's hardware model. + HardwareModel string `json:"hardware_model"` } type EnrollOrbitResponse struct { @@ -90,6 +94,8 @@ func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Ser Hostname: req.Hostname, Platform: req.Platform, OsqueryIdentifier: req.OsqueryIdentifier, + ComputerName: req.ComputerName, + HardwareModel: req.HardwareModel, }, req.EnrollSecret) if err != nil { return EnrollOrbitResponse{Err: err}, nil @@ -129,6 +135,8 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf "hostname", hostInfo.Hostname, "platform", hostInfo.Platform, "osquery_identifier", hostInfo.OsqueryIdentifier, + "computer_name", hostInfo.ComputerName, + "hardware_model", hostInfo.HardwareModel, ), level.Info, ) @@ -155,11 +163,22 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf return "", fleet.OrbitError{Message: "app config load failed: " + err.Error()} } - _, err = svc.ds.EnrollOrbit(ctx, appConfig.MDM.EnabledAndConfigured, hostInfo, orbitNodeKey, secret.TeamID) + host, err := svc.ds.EnrollOrbit(ctx, appConfig.MDM.EnabledAndConfigured, hostInfo, orbitNodeKey, secret.TeamID) if err != nil { return "", fleet.OrbitError{Message: "failed to enroll " + err.Error()} } + if err := svc.NewActivity( + ctx, + nil, + fleet.ActivityTypeFleetEnrolled{ + HostSerial: hostInfo.HardwareSerial, + HostDisplayName: host.DisplayName(), + }, + ); err != nil { + level.Error(svc.logger).Log("msg", "record fleet enroll activity", "err", err) + } + return orbitNodeKey, nil } diff --git a/server/service/orbit_client.go b/server/service/orbit_client.go index 4c13f07db5fa..bf355c9cd19d 100644 --- a/server/service/orbit_client.go +++ b/server/service/orbit_client.go @@ -456,6 +456,8 @@ func (oc *OrbitClient) enroll() (string, error) { Hostname: oc.hostInfo.Hostname, Platform: oc.hostInfo.Platform, OsqueryIdentifier: oc.hostInfo.OsqueryIdentifier, + ComputerName: oc.hostInfo.ComputerName, + HardwareModel: oc.hostInfo.HardwareModel, } var resp EnrollOrbitResponse err := oc.request(verb, path, params, &resp) From 42f759962e86b0eea44b0e1a1f3d54074c3e1eb4 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Mon, 18 Nov 2024 16:31:36 -0600 Subject: [PATCH 11/36] Fixed parsing Opera PE self-extracting archive. (#23751) (#23927) #23540 (cherry picked from commit 687ce3a71ac2acdef031470accdee69a9a8f4605) --- changes/23540-pe-sfx | 1 + go.mod | 3 ++- go.sum | 6 ++++-- pkg/file/pe.go | 44 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 changes/23540-pe-sfx diff --git a/changes/23540-pe-sfx b/changes/23540-pe-sfx new file mode 100644 index 000000000000..63c241a8be83 --- /dev/null +++ b/changes/23540-pe-sfx @@ -0,0 +1 @@ +Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera. diff --git a/go.mod b/go.mod index 822b953698bc..778e211419e0 100644 --- a/go.mod +++ b/go.mod @@ -92,7 +92,7 @@ require ( github.com/quasilyte/go-ruleguard/dsl v0.3.22 github.com/rs/zerolog v1.32.0 github.com/russellhaering/goxmldsig v1.2.0 - github.com/saferwall/pe v1.5.2 + github.com/saferwall/pe v1.5.5 github.com/sassoftware/relic/v8 v8.0.1 github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 github.com/sethvargo/go-password v0.3.0 @@ -292,6 +292,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect github.com/secure-systems-lab/go-securesystemslib v0.5.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/go.sum b/go.sum index 36ba36ea7db0..515f77b00eb9 100644 --- a/go.sum +++ b/go.sum @@ -1034,14 +1034,16 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/saferwall/pe v1.5.2 h1:h5lLtLsyxGHQ9dN6cd8EfeLEBEo5gdqJpkuw4o4vTMY= -github.com/saferwall/pe v1.5.2/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= +github.com/saferwall/pe v1.5.5 h1:GGbzKjXDm7i+1K6riOgtgblyTdRmTbr3r11IzjovAK8= +github.com/saferwall/pe v1.5.5/go.mod h1:mJx+PuptmNpoPFBNhWs/uDMFL/kTHVZIkg0d4OUJFbQ= github.com/sassoftware/relic/v8 v8.0.1 h1:uYUoaoTQMs67up8/46NgrSxSftgfY4VWBusDVg56k7I= github.com/sassoftware/relic/v8 v8.0.1/go.mod h1:s/MwugRcovgYcNJNOyvLfqRHDX7iArHtFtUR9kEodz8= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= +github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc= +github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA= github.com/secure-systems-lab/go-securesystemslib v0.5.0 h1:oTiNu0QnulMQgN/hLK124wJD/r2f9ZhIUuKIeBsCBT8= github.com/secure-systems-lab/go-securesystemslib v0.5.0/go.mod h1:uoCqUC0Ap7jrBSEanxT+SdACYJTVplRXWLkGMuDjXqk= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= diff --git a/pkg/file/pe.go b/pkg/file/pe.go index 27e872221d00..4516b5a25e15 100644 --- a/pkg/file/pe.go +++ b/pkg/file/pe.go @@ -48,21 +48,51 @@ func ExtractPEMetadata(tfr *fleet.TempFileReader) (*InstallerMetadata, error) { return nil, fmt.Errorf("error parsing PE file: %w", err) } - v, err := pep.ParseVersionResources() + resources, err := pep.ParseVersionResourcesForEntries() if err != nil { return nil, fmt.Errorf("error parsing PE version resources: %w", err) } - name := strings.TrimSpace(v["ProductName"]) + var name, version, sfxName, sfxVersion string + + for _, e := range resources { + productName, ok := e["ProductName"] + if !ok { + productName = e["productname"] // used by Opera SFX (self-extracting archive) + } + productVersion := strings.TrimSpace(e["ProductVersion"]) + if productName != "" { + productName = strings.TrimSpace(productName) + if productName == "7-Zip" { + // This may be a 7-Zip self-extracting archive. + sfxName = productName + sfxVersion = productVersion + continue + } + name = productName + } + if productVersion != "" { + version = productVersion + } + } + if name == "" && sfxName != "" { + // If we didn't find a ProductName, we may be + // dealing with an archive executable (e.g., if we're dealing with the 7-Zip executable itself rather than Opera) + name = sfxName + if sfxVersion != "" { + version = sfxVersion + } + } + return applySpecialCases(&InstallerMetadata{ Name: name, - Version: strings.TrimSpace(v["ProductVersion"]), + Version: version, PackageIDs: []string{name}, SHASum: h.Sum(nil), - }, v), nil + }, resources), nil } -var exeSpecialCases = map[string]func(*InstallerMetadata, map[string]string) *InstallerMetadata{ - "Notion": func(meta *InstallerMetadata, resources map[string]string) *InstallerMetadata { +var exeSpecialCases = map[string]func(*InstallerMetadata, []map[string]string) *InstallerMetadata{ + "Notion": func(meta *InstallerMetadata, _ []map[string]string) *InstallerMetadata { if meta.Version != "" { meta.Name = meta.Name + " " + meta.Version } @@ -82,7 +112,7 @@ var exeSpecialCases = map[string]func(*InstallerMetadata, map[string]string) *In // least for the most popular apps that use unusual naming. // // See https://github.com/fleetdm/fleet/issues/20440#issuecomment-2260500661 -func applySpecialCases(meta *InstallerMetadata, resources map[string]string) *InstallerMetadata { +func applySpecialCases(meta *InstallerMetadata, resources []map[string]string) *InstallerMetadata { if fn := exeSpecialCases[meta.Name]; fn != nil { return fn(meta, resources) } From f66256ac86fbb4e76a7b4851f4b8e0a803c0c071 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Mon, 18 Nov 2024 16:39:13 -0600 Subject: [PATCH 12/36] Adding changes for Fleet v4.59.1 (#23862) (#23929) (#23930) --- CHANGELOG.md | 6 ++++++ charts/fleet/Chart.yaml | 4 ++-- charts/fleet/values.yaml | 2 +- infrastructure/dogfood/terraform/aws/variables.tf | 2 +- infrastructure/dogfood/terraform/gcp/variables.tf | 2 +- infrastructure/guardduty/.terraform.lock.hcl | 4 ++-- infrastructure/guardduty/main.tf | 2 +- infrastructure/infrastructure/cloudtrail/main.tf | 2 +- .../infrastructure/elastic-agent/.terraform.lock.hcl | 4 ++-- infrastructure/infrastructure/elastic-agent/main.tf | 2 +- .../infrastructure/guardduty-alerts/.terraform.lock.hcl | 4 ++-- infrastructure/infrastructure/guardduty-alerts/main.tf | 2 +- infrastructure/infrastructure/spend_alerts/main.tf | 2 +- terraform/addons/vuln-processing/variables.tf | 4 ++-- terraform/byo-vpc/byo-db/byo-ecs/variables.tf | 4 ++-- terraform/byo-vpc/byo-db/variables.tf | 4 ++-- terraform/byo-vpc/example/main.tf | 2 +- terraform/byo-vpc/variables.tf | 4 ++-- terraform/example/main.tf | 4 ++-- terraform/variables.tf | 4 ++-- tools/fleetctl-npm/package.json | 2 +- 21 files changed, 36 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46bf107edfa2..ec554cea3507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Fleet 4.59.1 (Nov 18, 2024) + +### Bug fixes + +* Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint. + ## Fleet 4.59.0 (Nov 12, 2024) ### Endpoint operations diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index 0a7be2bfdb94..f9b80ed1e343 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -4,11 +4,11 @@ name: fleet keywords: - fleet - osquery -version: v6.2.1 +version: v6.2.2 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.59.0 +appVersion: v4.59.1 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index da975de66ad3..7e1c7f7916b2 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -3,7 +3,7 @@ hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy imageRepository: fleetdm/fleet -imageTag: v4.59.0 # Version of Fleet to deploy +imageTag: v4.59.1 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index f81ed35ca06c..cd04b77c5028 100644 --- a/infrastructure/dogfood/terraform/aws/variables.tf +++ b/infrastructure/dogfood/terraform/aws/variables.tf @@ -56,7 +56,7 @@ variable "database_name" { variable "fleet_image" { description = "the name of the container image to run" - default = "fleetdm/fleet:v4.59.0" + default = "fleetdm/fleet:v4.59.1" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index 6e690ee8a54a..deb96bc38ec1 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,7 +68,7 @@ variable "redis_mem" { } variable "image" { - default = "fleetdm/fleet:v4.59.0" + default = "fleetdm/fleet:v4.59.1" } variable "software_installers_bucket_name" { diff --git a/infrastructure/guardduty/.terraform.lock.hcl b/infrastructure/guardduty/.terraform.lock.hcl index c58c51094906..5b743eb544e9 100644 --- a/infrastructure/guardduty/.terraform.lock.hcl +++ b/infrastructure/guardduty/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.59.0" - constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.0" + version = "4.59.1" + constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.1" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/guardduty/main.tf b/infrastructure/guardduty/main.tf index b20182929594..fdeb7607e00e 100644 --- a/infrastructure/guardduty/main.tf +++ b/infrastructure/guardduty/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.0" + version = "~> 4.59.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/cloudtrail/main.tf b/infrastructure/infrastructure/cloudtrail/main.tf index cce55a6999df..0eaff5aff2ea 100644 --- a/infrastructure/infrastructure/cloudtrail/main.tf +++ b/infrastructure/infrastructure/cloudtrail/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.0" + version = "~> 4.59.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl index ec59d9ece644..c327efe67542 100644 --- a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl +++ b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.59.0" - constraints = ">= 3.63.0, ~> 4.59.0" + version = "4.59.1" + constraints = ">= 3.63.0, ~> 4.59.1" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/infrastructure/elastic-agent/main.tf b/infrastructure/infrastructure/elastic-agent/main.tf index ae7ef8d89026..78f310682be3 100644 --- a/infrastructure/infrastructure/elastic-agent/main.tf +++ b/infrastructure/infrastructure/elastic-agent/main.tf @@ -20,7 +20,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.0" + version = "~> 4.59.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl index c58c51094906..5b743eb544e9 100644 --- a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl +++ b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.59.0" - constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.0" + version = "4.59.1" + constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.1" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/infrastructure/guardduty-alerts/main.tf b/infrastructure/infrastructure/guardduty-alerts/main.tf index 1e1ee8abe2c3..4d0e0f4a6805 100644 --- a/infrastructure/infrastructure/guardduty-alerts/main.tf +++ b/infrastructure/infrastructure/guardduty-alerts/main.tf @@ -15,7 +15,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.0" + version = "~> 4.59.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/spend_alerts/main.tf b/infrastructure/infrastructure/spend_alerts/main.tf index 1754cfdf8cdf..837d69399e1a 100644 --- a/infrastructure/infrastructure/spend_alerts/main.tf +++ b/infrastructure/infrastructure/spend_alerts/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.0" + version = "~> 4.59.1" } } backend "s3" { diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf index 17947300da25..b372a36ff8d4 100644 --- a/terraform/addons/vuln-processing/variables.tf +++ b/terraform/addons/vuln-processing/variables.tf @@ -24,7 +24,7 @@ variable "fleet_config" { vuln_processing_cpu = optional(number, 2048) vuln_data_stream_mem = optional(number, 1024) vuln_data_stream_cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.59.0") + image = optional(string, "fleetdm/fleet:v4.59.1") family = optional(string, "fleet-vuln-processing") sidecars = optional(list(any), []) extra_environment_variables = optional(map(string), {}) @@ -82,7 +82,7 @@ variable "fleet_config" { vuln_processing_cpu = 2048 vuln_data_stream_mem = 1024 vuln_data_stream_cpu = 512 - image = "fleetdm/fleet:v4.59.0" + image = "fleetdm/fleet:v4.59.1" family = "fleet-vuln-processing" sidecars = [] extra_environment_variables = {} diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 049841c1f735..580e94cbf51d 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -16,7 +16,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.0") + image = optional(string, "fleetdm/fleet:v4.59.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -119,7 +119,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.0" + image = "fleetdm/fleet:v4.59.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index 20040d516caf..ddd474e14ba7 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -77,7 +77,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.0") + image = optional(string, "fleetdm/fleet:v4.59.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -205,7 +205,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.0" + image = "fleetdm/fleet:v4.59.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 8b0eefb3bea0..855ab59f9fc0 100644 --- a/terraform/byo-vpc/example/main.tf +++ b/terraform/byo-vpc/example/main.tf @@ -17,7 +17,7 @@ provider "aws" { } locals { - fleet_image = "fleetdm/fleet:v4.59.0" + fleet_image = "fleetdm/fleet:v4.59.1" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index 593d3a390ffc..f85ddb408381 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -170,7 +170,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.0") + image = optional(string, "fleetdm/fleet:v4.59.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -298,7 +298,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.0" + image = "fleetdm/fleet:v4.59.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/example/main.tf b/terraform/example/main.tf index 8b92f669bece..7f169df3e89b 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -63,8 +63,8 @@ module "fleet" { fleet_config = { # To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror - # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.59.0" - image = "fleetdm/fleet:v4.59.0" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.59.1" + image = "fleetdm/fleet:v4.59.1" # override default to deploy the image you desire # See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling # memory and cpu. autoscaling = { diff --git a/terraform/variables.tf b/terraform/variables.tf index 34c6d7a1f58c..9f08b701df38 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -218,7 +218,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.0") + image = optional(string, "fleetdm/fleet:v4.59.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -346,7 +346,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.0" + image = "fleetdm/fleet:v4.59.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index 4832a3f837ca..d9d07156ca18 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.59.0", + "version": "v4.59.1", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" From 692665487baccea9a537e515b1b4fd64ad6aaa78 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:22:56 -0500 Subject: [PATCH 13/36] For R.C. - Fleet UI: Fix unreleased bug of team dropdown width being static 80px (#23925) --- frontend/components/TeamsDropdown/TeamsDropdown.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx index 3e134954ca11..e38b1c1b13f3 100644 --- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx +++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx @@ -113,10 +113,6 @@ const TeamsDropdown = ({ }; const customStyles: StylesConfig = { - container: (provided) => ({ - ...provided, - width: "80px", - }), control: (provided, state) => ({ ...provided, display: "flex", From 319b15a0cf4b64119b0ffe59f3be1f00c299b86c Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Wed, 20 Nov 2024 12:56:45 -0600 Subject: [PATCH 14/36] Cherry-Pick: Endpoint changes for LUKS escrow trigger, Orbit notification, LUKS escrow persistence + retrieval (#23980) Cherry-pick for #22583, #22584. Original commits to `main` from PRs #23763, #23938, #23952 --------- Co-authored-by: Jacob Shandling --- ee/server/service/devices.go | 48 ++++ server/datastore/mysql/hosts.go | 54 ++++ server/datastore/mysql/hosts_test.go | 96 +++++++ ...322_AddLuksDataToHostDiskEncryptionKeys.go | 25 ++ server/datastore/mysql/schema.sql | 6 +- server/fleet/capabilities.go | 14 +- server/fleet/datastore.go | 12 +- server/fleet/hosts.go | 8 +- server/fleet/orbit.go | 3 + server/fleet/service.go | 5 + server/fleet/utils.go | 26 ++ server/mdm/apple/gdmf/api.go | 3 +- server/mdm/apple/util.go | 19 -- server/mdm/mdm.go | 54 ++++ server/mock/datastore_mock.go | 78 ++++- server/service/devices.go | 37 +++ server/service/devices_test.go | 109 +++++++ server/service/handler.go | 6 + server/service/hosts.go | 68 +++-- server/service/hosts_test.go | 68 +++++ server/service/orbit.go | 93 ++++++ server/service/orbit_test.go | 267 +++++++++++++++++- 22 files changed, 1044 insertions(+), 55 deletions(-) create mode 100644 server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 7c3b580e92b3..9fa82d0e556c 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -166,3 +166,51 @@ func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSu return sum, nil } + +func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error { + if svc.ds.IsHostPendingEscrow(ctx, host.ID) { + return nil + } + + if err := svc.validateReadyForLinuxEscrow(ctx, host); err != nil { + _ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error()) + return err + } + + return svc.ds.QueueEscrow(ctx, host.ID) +} + +func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet.Host) error { + if !host.IsLUKSSupported() { + return &fleet.BadRequestError{Message: "Host platform does not support key escrow"} + } + + ac, err := svc.ds.AppConfig(ctx) + if err != nil { + return err + } + + if host.TeamID == nil { + if !ac.MDM.EnableDiskEncryption.Value { + return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team"} + } + } else { + tc, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID) + if err != nil { + return err + } + if !tc.EnableDiskEncryption { + return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team"} + } + } + + if host.DiskEncryptionEnabled == nil || !*host.DiskEncryptionEnabled { + return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please enable disk encryption for this host."} + } + + if host.OrbitVersion == nil || !fleet.IsAtLeastVersion(*host.OrbitVersion, fleet.MinOrbitLUKSVersion) { + return &fleet.BadRequestError{Message: "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version."} + } + + return svc.ds.AssertHasNoEncryptionKeyStored(ctx, host.ID) +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index ee189ebef478..c5d74735f29b 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3819,6 +3819,60 @@ ON DUPLICATE KEY UPDATE return err } +func (ds *Datastore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error { + if encryptedBase64Passphrase == "" || encryptedBase64Salt == "" { // should have been caught at service level + return errors.New("passphrase and salt must be set") + } + + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, base64_encrypted_salt, key_slot, client_error, decryptable) +VALUES + (?, ?, ?, ?, '', TRUE) +ON DUPLICATE KEY UPDATE + decryptable = TRUE, + base64_encrypted = VALUES(base64_encrypted), + base64_encrypted_salt = VALUES(base64_encrypted_salt), + key_slot = VALUES(key_slot), + client_error = '' +`, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot) + return err +} +func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool { + var pendingEscrowCount uint + _ = sqlx.GetContext(ctx, ds.reader(ctx), &pendingEscrowCount, ` + SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND reset_requested = TRUE`, hostID) + return pendingEscrowCount > 0 +} +func (ds *Datastore) ClearPendingEscrow(ctx context.Context, hostID uint) error { + _, err := ds.writer(ctx).ExecContext(ctx, `UPDATE host_disk_encryption_keys SET reset_requested = FALSE WHERE host_id = ?`, hostID) + return err +} +func (ds *Datastore) ReportEscrowError(ctx context.Context, hostID uint, errorMessage string) error { + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, client_error) VALUES (?, '', ?) ON DUPLICATE KEY UPDATE client_error = VALUES(client_error) +`, hostID, errorMessage) + return err +} +func (ds *Datastore) QueueEscrow(ctx context.Context, hostID uint) error { + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, reset_requested) VALUES (?, '', TRUE) ON DUPLICATE KEY UPDATE reset_requested = TRUE +`, hostID) + return err +} +func (ds *Datastore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error { + var hasKeyCount uint + err := sqlx.GetContext(ctx, ds.reader(ctx), &hasKeyCount, ` + SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND base64_encrypted != ''`, hostID) + if hasKeyCount > 0 { + return &fleet.BadRequestError{Message: "Key has already been escrowed for this host"} + } + + return err +} + func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { // NOTE(mna): currently we only verify encryption keys for macOS, // Windows/bitlocker uses a different approach where orbit sends the diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 4e0269ad3588..6fe89b476839 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -155,6 +155,7 @@ func TestHosts(t *testing.T) { {"SetOrUpdateHostDiskEncryptionKeys", testHostsSetOrUpdateHostDisksEncryptionKey}, {"SetHostsDiskEncryptionKeyStatus", testHostsSetDiskEncryptionKeyStatus}, {"GetUnverifiedDiskEncryptionKeys", testHostsGetUnverifiedDiskEncryptionKeys}, + {"LUKS", testLUKSDatastoreFunctions}, {"EnrollOrbit", testHostsEnrollOrbit}, {"EnrollUpdatesMissingInfo", testHostsEnrollUpdatesMissingInfo}, {"EncryptionKeyRawDecryption", testHostsEncryptionKeyRawDecryption}, @@ -7807,6 +7808,101 @@ func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected require.Equal(t, expectedDecryptable, got.Decryptable) } +func testLUKSDatastoreFunctions(t *testing.T, ds *Datastore) { + ctx := context.Background() + + host1, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("1"), + UUID: "1", + OsqueryHostID: ptr.String("1"), + Hostname: "foo.local", + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + }) + require.NoError(t, err) + host2, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("2"), + UUID: "2", + OsqueryHostID: ptr.String("2"), + Hostname: "foo.local2", + PrimaryIP: "192.168.1.2", + PrimaryMac: "30-65-EC-6F-C4-59", + }) + require.NoError(t, err) + host3, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("3"), + UUID: "3", + OsqueryHostID: ptr.String("3"), + Hostname: "foo.local3", + PrimaryIP: "192.168.1.3", + PrimaryMac: "30-65-EC-6F-C4-60", + }) + require.NoError(t, err) + + // queue shows as pending + require.False(t, ds.IsHostPendingEscrow(ctx, host1.ID)) + err = ds.QueueEscrow(ctx, host1.ID) + require.NoError(t, err) + require.False(t, ds.IsHostPendingEscrow(ctx, host2.ID)) + require.True(t, ds.IsHostPendingEscrow(ctx, host1.ID)) + + // clear removes pending + err = ds.QueueEscrow(ctx, host2.ID) + require.NoError(t, err) + err = ds.ClearPendingEscrow(ctx, host1.ID) + require.NoError(t, err) + require.False(t, ds.IsHostPendingEscrow(ctx, host1.ID)) + require.True(t, ds.IsHostPendingEscrow(ctx, host2.ID)) + + // report escrow error does not remove pending + err = ds.ReportEscrowError(ctx, host2.ID, "this broke") + require.NoError(t, err) + require.True(t, ds.IsHostPendingEscrow(ctx, host2.ID)) + // TODO confirm error was persisted + + // assert no key stored on hosts with varying no-key-stored states + require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) + require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID)) + require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID)) + + // no change when blank key or salt attempted to save + err = ds.SaveLUKSData(ctx, host1.ID, "", "", 0) + require.Error(t, err) + require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) + err = ds.SaveLUKSData(ctx, host1.ID, "foo", "", 0) + require.Error(t, err) + require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) + + // persists with passphrase and salt set + err = ds.SaveLUKSData(ctx, host2.ID, "bazqux", "fuzzmuffin", 0) + require.NoError(t, err) + require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) + require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID)) + key, err := ds.GetHostDiskEncryptionKey(ctx, host2.ID) + require.NoError(t, err) + require.Equal(t, "bazqux", key.Base64Encrypted) + + // persists when host hasn't had anything queued + err = ds.SaveLUKSData(ctx, host3.ID, "newstuff", "fuzzball", 1) + require.NoError(t, err) + require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID)) + key, err = ds.GetHostDiskEncryptionKey(ctx, host3.ID) + require.NoError(t, err) + require.Equal(t, "newstuff", key.Base64Encrypted) +} + func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { host, err := ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), diff --git a/server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go b/server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go new file mode 100644 index 000000000000..7a790a454099 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go @@ -0,0 +1,25 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20241116233322, Down_20241116233322) +} + +func Up_20241116233322(tx *sql.Tx) error { + _, err := tx.Exec(`ALTER TABLE host_disk_encryption_keys + ADD COLUMN base64_encrypted_salt VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' AFTER base64_encrypted, + ADD COLUMN key_slot TINYINT UNSIGNED DEFAULT NULL AFTER base64_encrypted_salt`) + if err != nil { + return fmt.Errorf("failed to add base64_encrypted_salt and key_slot columns to host_disk_encryption_keys: %w", err) + } + + return nil +} + +func Down_20241116233322(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index e9740f5e9d2d..45718cbd1d7c 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -303,6 +303,8 @@ CREATE TABLE `host_device_auth` ( CREATE TABLE `host_disk_encryption_keys` ( `host_id` int unsigned NOT NULL, `base64_encrypted` text COLLATE utf8mb4_unicode_ci NOT NULL, + `base64_encrypted_salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `key_slot` tinyint unsigned DEFAULT NULL, `decryptable` tinyint(1) DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, @@ -1102,9 +1104,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=330 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=331 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/capabilities.go b/server/fleet/capabilities.go index 7da6f3620522..d8abea773166 100644 --- a/server/fleet/capabilities.go +++ b/server/fleet/capabilities.go @@ -80,6 +80,9 @@ const ( CapabilityEndUserEmail Capability = "end_user_email" // CapabilityEscrowBuddy allows to use Escrow Buddy to rotate FileVault keys CapabilityEscrowBuddy Capability = "escrow_buddy" + // CapabilityLinuxDiskEncryptionEscrow denotes the ability of the server to escrow Ubuntu and Fedora disk + // encryption LUKS passphrases + CapabilityLinuxDiskEncryptionEscrow Capability = "linux_disk_encryption_escrow" // CapabilitySetupExperience denotes the ability of the server to support // installing software and running a script during macOS ADE enrollment, and // the ability of the client to show the corresponding UI to support that @@ -89,11 +92,12 @@ const ( func GetServerOrbitCapabilities() CapabilityMap { return CapabilityMap{ - CapabilityOrbitEndpoints: {}, - CapabilityTokenRotation: {}, - CapabilityEndUserEmail: {}, - CapabilityEscrowBuddy: {}, - CapabilitySetupExperience: {}, + CapabilityOrbitEndpoints: {}, + CapabilityTokenRotation: {}, + CapabilityEndUserEmail: {}, + CapabilityEscrowBuddy: {}, + CapabilityLinuxDiskEncryptionEscrow: {}, + CapabilitySetupExperience: {}, } } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 2cfaf6deb3ff..6464b4b51c97 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -898,19 +898,29 @@ type Datastore interface { // GetHostEmails returns the emails associated with the provided host for a given source, such as "google_chrome_profiles" GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error + SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for // a host SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error + // SaveLUKSData sets base64'd encrypted LUKS passphrase, key slot, and salt data for a host that has successfully + // escrowed LUKS data + SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error + // GetUnverifiedDiskEncryptionKeys returns all the encryption keys that // are collected but their decryptable status is not known yet (ie: // we're able to decrypt the key using a private key in the server) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]HostDiskEncryptionKey, error) // SetHostsDiskEncryptionKeyStatus sets the encryptable status for the set // of encription keys provided - SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error + SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error // GetHostDiskEncryptionKey returns the encryption key information for a given host GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*HostDiskEncryptionKey, error) + IsHostPendingEscrow(ctx context.Context, hostID uint) bool + ClearPendingEscrow(ctx context.Context, hostID uint) error + ReportEscrowError(ctx context.Context, hostID uint, err string) error + QueueEscrow(ctx context.Context, hostID uint) error + AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error // GetHostCertAssociationsToExpire retrieves host certificate // associations that are close to expire and don't have a renewal in diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index e402af547218..407af288f825 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -322,7 +322,7 @@ type Host struct { // DiskEncryptionEnabled is only returned by GET /host/{id} and so is not // exportable as CSV (which is the result of List Hosts endpoint). It is - // a *bool because for Linux we set it to NULL and omit it from the JSON + // a *bool because for some Linux we set it to NULL and omit it from the JSON // response if the host does not have disk encryption enabled. It is also // omitted if we don't have encryption information yet. DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled" csv:"-"` @@ -682,6 +682,12 @@ func (h *Host) IsDEPAssignedToFleet() bool { return h.DEPAssignedToFleet != nil && *h.DEPAssignedToFleet } +// IsLUKSSupported returns true if the host's platform is Linux and running +// one of the supported OS versions. +func (h *Host) IsLUKSSupported() bool { + return h.Platform == "ubuntu" || strings.Contains(h.OSVersion, "Fedora") // fedora h.Platform reports as "rhel" +} + // IsEligibleForWindowsMDMUnenrollment returns true if the host must be // unenrolled from Fleet's Windows MDM (if it MDM was disabled). func (h *Host) IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM bool) bool { diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index e3c0c7b0db3e..6c06a963e263 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -40,6 +40,9 @@ type OrbitConfigNotifications struct { // RunSetupExperience indicates whether or not Orbit should run the Fleet setup experience // during macOS Setup Assistant. RunSetupExperience bool `json:"run_setup_experience,omitempty"` + + // RunDiskEncryptionEscrow tells Orbit to prompt the end user to escrow disk encryption data + RunDiskEncryptionEscrow bool `json:"run_disk_encryption_escrow,omitempty"` } type OrbitConfig struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index 285ddac74d7a..9eb03ec2f73e 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -394,6 +394,7 @@ type Service interface { GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error) HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error) + EscrowLUKSData(ctx context.Context, passphrase string, salt string, keySlot *uint, clientError string) error // AddLabelsToHost adds the given label names to the host's label membership. // @@ -964,6 +965,8 @@ type Service interface { GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error) + TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *Host) error + // CheckMDMAppleEnrollmentWithMinimumOSVersion checks if the minimum OS version is met for a MDM enrollment CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx context.Context, m *MDMAppleMachineInfo) (*MDMAppleSoftwareUpdateRequired, error) @@ -1169,4 +1172,6 @@ const ( BatchSetSoftwareInstallersStatusCompleted = "completed" // BatchSetSoftwareInstallerStatusFailed is the value returned for a failed BatchSetSoftwareInstallers operation. BatchSetSoftwareInstallersStatusFailed = "failed" + // MinOrbitLUKSVersion is the earliest version of Orbit that can escrow LUKS passphrases + MinOrbitLUKSVersion = "1.36.0" ) diff --git a/server/fleet/utils.go b/server/fleet/utils.go index ddab26e8e797..fb286b56a764 100644 --- a/server/fleet/utils.go +++ b/server/fleet/utils.go @@ -6,6 +6,7 @@ import ( "io" "strings" + "github.com/Masterminds/semver" "github.com/fatih/color" "golang.org/x/text/unicode/norm" ) @@ -65,3 +66,28 @@ func Preprocess(input string) string { // Normalize Unicode characters. return norm.NFC.String(input) } + +// CompareVersions returns an integer comparing two versions according to semantic version +// precedence. The result will be 0 if a == b, -1 if a < b, or +1 if a > b. +// An invalid semantic version string is considered less than a valid one. All invalid semantic +// version strings compare equal to each other. +func CompareVersions(a string, b string) int { + verA, errA := semver.NewVersion(a) + verB, errB := semver.NewVersion(b) + switch { + case errA != nil && errB != nil: + return 0 + case errA != nil: + return -1 + case errB != nil: + return 1 + default: + return verA.Compare(verB) + } +} + +// IsAtLeastVersion returns whether currentVersion is at least minimumVersion, using semantics +// of CompareVersions for version validity +func IsAtLeastVersion(currentVersion string, minimumVersion string) bool { + return CompareVersions(currentVersion, minimumVersion) >= 0 +} diff --git a/server/mdm/apple/gdmf/api.go b/server/mdm/apple/gdmf/api.go index ee8c671814cd..d969c2580081 100644 --- a/server/mdm/apple/gdmf/api.go +++ b/server/mdm/apple/gdmf/api.go @@ -15,7 +15,6 @@ import ( "github.com/cenkalti/backoff" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/fleet" - apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" ) const baseURL = "https://gdmf.apple.com/v2/pmv" @@ -91,7 +90,7 @@ func GetLatestOSVersion(device fleet.MDMAppleMachineInfo) (*Asset, error) { latestIdx = i // first match found, update the index continue } - if apple_mdm.CompareVersions(assetSet[latestIdx].ProductVersion, s.ProductVersion) < 0 { + if fleet.CompareVersions(assetSet[latestIdx].ProductVersion, s.ProductVersion) < 0 { latestIdx = i // found a later version, update the index } } diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go index 88267f953fa3..200e1d4f39ad 100644 --- a/server/mdm/apple/util.go +++ b/server/mdm/apple/util.go @@ -124,22 +124,3 @@ func IsLessThanVersion(current string, target string) (bool, error) { return cv.LessThan(tv), nil } - -// CompareVersions returns an integer comparing two versions according to semantic version -// precedence. The result will be 0 if a == b, -1 if a < b, or +1 if a > b. -// An invalid semantic version string is considered less than a valid one. All invalid semantic -// version strings compare equal to each other. -func CompareVersions(a string, b string) int { - verA, errA := semver.NewVersion(a) - verB, errB := semver.NewVersion(b) - switch { - case errA != nil && errB != nil: - return 0 - case errA != nil: - return -1 - case errB != nil: - return 1 - default: - return verA.Compare(verB) - } -} diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 5aaae483d8e6..cf800dcfa690 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -3,8 +3,13 @@ package mdm import ( "bytes" "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" "crypto/x509" "encoding/base64" + "fmt" + "io" "github.com/smallstep/pkcs7" ) @@ -81,6 +86,55 @@ func GuessProfileExtension(profile []byte) string { } } +func EncryptAndEncode(plainText string, symmetricKey string) (string, error) { + block, err := aes.NewCipher([]byte(symmetricKey)) + if err != nil { + return "", fmt.Errorf("create new cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("create new gcm: %w", err) + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("generate nonce: %w", err) + } + + return base64.StdEncoding.EncodeToString(aesGCM.Seal(nonce, nonce, []byte(plainText), nil)), nil +} + +func DecodeAndDecrypt(base64CipherText string, symmetricKey string) (string, error) { + encrypted, err := base64.StdEncoding.DecodeString(base64CipherText) + if err != nil { + return "", fmt.Errorf("base64 decode: %w", err) + } + + block, err := aes.NewCipher([]byte(symmetricKey)) + if err != nil { + return "", fmt.Errorf("create new cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("create new gcm: %w", err) + } + + // Get the nonce size + nonceSize := aesGCM.NonceSize() + + // Extract the nonce from the encrypted data + nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:] + + decrypted, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", fmt.Errorf("decrypting: %w", err) + } + + return string(decrypted), nil +} + const ( // FleetdConfigProfileName is the value for the PayloadDisplayName used by // fleetd to read configuration values from the system. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index ad49d7a6aa28..23b95fb5990c 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -639,12 +639,24 @@ type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, e type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error +type SaveLUKSDataFunc func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error + type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) -type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error +type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) +type IsHostPendingEscrowFunc func(ctx context.Context, hostID uint) bool + +type ClearPendingEscrowFunc func(ctx context.Context, hostID uint) error + +type ReportEscrowErrorFunc func(ctx context.Context, hostID uint, err string) error + +type QueueEscrowFunc func(ctx context.Context, hostID uint) error + +type AssertHasNoEncryptionKeyStoredFunc func(ctx context.Context, hostID uint) error + type GetHostCertAssociationsToExpireFunc func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) type SetCommandForPendingSCEPRenewalFunc func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error @@ -2077,6 +2089,9 @@ type DataStore struct { SetOrUpdateHostDiskEncryptionKeyFunc SetOrUpdateHostDiskEncryptionKeyFunc SetOrUpdateHostDiskEncryptionKeyFuncInvoked bool + SaveLUKSDataFunc SaveLUKSDataFunc + SaveLUKSDataFuncInvoked bool + GetUnverifiedDiskEncryptionKeysFunc GetUnverifiedDiskEncryptionKeysFunc GetUnverifiedDiskEncryptionKeysFuncInvoked bool @@ -2086,6 +2101,21 @@ type DataStore struct { GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFuncInvoked bool + IsHostPendingEscrowFunc IsHostPendingEscrowFunc + IsHostPendingEscrowFuncInvoked bool + + ClearPendingEscrowFunc ClearPendingEscrowFunc + ClearPendingEscrowFuncInvoked bool + + ReportEscrowErrorFunc ReportEscrowErrorFunc + ReportEscrowErrorFuncInvoked bool + + QueueEscrowFunc QueueEscrowFunc + QueueEscrowFuncInvoked bool + + AssertHasNoEncryptionKeyStoredFunc AssertHasNoEncryptionKeyStoredFunc + AssertHasNoEncryptionKeyStoredFuncInvoked bool + GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFuncInvoked bool @@ -5008,6 +5038,13 @@ func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key, clientError, decryptable) } +func (s *DataStore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error { + s.mu.Lock() + s.SaveLUKSDataFuncInvoked = true + s.mu.Unlock() + return s.SaveLUKSDataFunc(ctx, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot) +} + func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { s.mu.Lock() s.GetUnverifiedDiskEncryptionKeysFuncInvoked = true @@ -5015,11 +5052,11 @@ func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]flee return s.GetUnverifiedDiskEncryptionKeysFunc(ctx) } -func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error { +func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error { s.mu.Lock() s.SetHostsDiskEncryptionKeyStatusFuncInvoked = true s.mu.Unlock() - return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, encryptable, threshold) + return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, decryptable, threshold) } func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) { @@ -5029,6 +5066,41 @@ func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) ( return s.GetHostDiskEncryptionKeyFunc(ctx, hostID) } +func (s *DataStore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool { + s.mu.Lock() + s.IsHostPendingEscrowFuncInvoked = true + s.mu.Unlock() + return s.IsHostPendingEscrowFunc(ctx, hostID) +} + +func (s *DataStore) ClearPendingEscrow(ctx context.Context, hostID uint) error { + s.mu.Lock() + s.ClearPendingEscrowFuncInvoked = true + s.mu.Unlock() + return s.ClearPendingEscrowFunc(ctx, hostID) +} + +func (s *DataStore) ReportEscrowError(ctx context.Context, hostID uint, err string) error { + s.mu.Lock() + s.ReportEscrowErrorFuncInvoked = true + s.mu.Unlock() + return s.ReportEscrowErrorFunc(ctx, hostID, err) +} + +func (s *DataStore) QueueEscrow(ctx context.Context, hostID uint) error { + s.mu.Lock() + s.QueueEscrowFuncInvoked = true + s.mu.Unlock() + return s.QueueEscrowFunc(ctx, hostID) +} + +func (s *DataStore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error { + s.mu.Lock() + s.AssertHasNoEncryptionKeyStoredFuncInvoked = true + s.mu.Unlock() + return s.AssertHasNoEncryptionKeyStoredFunc(ctx, hostID) +} + func (s *DataStore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) { s.mu.Lock() s.GetHostCertAssociationsToExpireFuncInvoked = true diff --git a/server/service/devices.go b/server/service/devices.go index 3ae57851b102..187e168bf48a 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -609,6 +609,43 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos return fleet.ErrMissingLicense } +//////////////////////////////////////////////////////////////////////////////// +// Trigger linux key escrow +//////////////////////////////////////////////////////////////////////////////// + +type triggerLinuxDiskEncryptionEscrowRequest struct { + Token string `url:"token"` +} + +func (r *triggerLinuxDiskEncryptionEscrowRequest) deviceAuthToken() string { + return r.Token +} + +type triggerLinuxDiskEncryptionEscrowResponse struct { + Err error `json:"error,omitempty"` +} + +func (r triggerLinuxDiskEncryptionEscrowResponse) error() error { return r.Err } + +func (r triggerLinuxDiskEncryptionEscrowResponse) Status() int { return http.StatusNoContent } + +func triggerLinuxDiskEncryptionEscrowEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + host, ok := hostctx.FromContext(ctx) + if !ok { + err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) + return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil + } + + if err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host); err != nil { + return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil + } + return triggerLinuxDiskEncryptionEscrowResponse{}, nil +} + +func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error { + return fleet.ErrMissingLicense +} + //////////////////////////////////////////////////////////////////////////////// // Get Current Device's Software //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/devices_test.go b/server/service/devices_test.go index 1100683be4fb..774a941ca06f 100644 --- a/server/service/devices_test.go +++ b/server/service/devices_test.go @@ -3,10 +3,12 @@ package service import ( "context" "database/sql" + "errors" "fmt" "testing" "time" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" @@ -475,3 +477,110 @@ func TestGetFleetDesktopSummary(t *testing.T) { }) } + +func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { + t.Run("unavailable in Fleet Free", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{SkipCreateTestUsers: true}) + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, &fleet.Host{ID: 1}) + require.ErrorIs(t, err, fleet.ErrMissingLicense) + }) + + t.Run("no-op on already pending", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true}) + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return true + } + + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, &fleet.Host{ID: 1}) + require.NoError(t, err) + require.True(t, ds.IsHostPendingEscrowFuncInvoked) + }) + + t.Run("validation failures", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true}) + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } + var reportedErrors []string + host := &fleet.Host{ID: 1, Platform: "rhel", OSVersion: "Red Hat Enterprise Linux 9.0.0"} + ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error { + require.Equal(t, hostID, host.ID) + reportedErrors = append(reportedErrors, err) + return nil + } + + // invalid platform + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host platform does not support key escrow") + require.True(t, ds.IsHostPendingEscrowFuncInvoked) + + // valid platform, no-team, encryption not enabled + host.OSVersion = "Fedora 32.0.0" + appConfig := &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(false)}} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appConfig, nil + } + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Disk encryption is not enabled for hosts not assigned to a team") + + // valid platform, team, encryption not enabled + host.TeamID = ptr.Uint(1) + teamConfig := &fleet.TeamMDM{} + ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { + require.Equal(t, uint(1), teamID) + return teamConfig, nil + } + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Disk encryption is not enabled for this host's team") + + // valid platform, team, host disk is not encrypted or unknown encryption state + teamConfig = &fleet.TeamMDM{EnableDiskEncryption: true} + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + host.DiskEncryptionEnabled = ptr.Bool(false) + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + + // Orbit version is too old + host.DiskEncryptionEnabled = ptr.Bool(true) + host.OrbitVersion = ptr.String("1.35.1") + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version.") + + // Encryption key is already escrowed + host.OrbitVersion = ptr.String(fleet.MinOrbitLUKSVersion) + ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error { + return errors.New("encryption key is already escrowed") + } + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "encryption key is already escrowed") + + require.Len(t, reportedErrors, 7) + }) + + t.Run("validation success", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true}) + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}}, nil + } + ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error { + return nil + } + host := &fleet.Host{ID: 1, Platform: "ubuntu", DiskEncryptionEnabled: ptr.Bool(true), OrbitVersion: ptr.String(fleet.MinOrbitLUKSVersion)} + ds.QueueEscrowFunc = func(ctx context.Context, hostID uint) error { + require.Equal(t, uint(1), hostID) + return nil + } + + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.NoError(t, err) + require.True(t, ds.QueueEscrowFuncInvoked) + }) +} diff --git a/server/service/handler.go b/server/service/handler.go index 4f916f576ba2..147acacf4402 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -837,6 +837,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC errorLimiter.Limit("post_device_migrate_mdm", desktopQuota), ).POST("/api/_version_/fleet/device/{token}/migrate_mdm", migrateMDMDeviceEndpoint, deviceMigrateMDMRequest{}) + de.WithCustomMiddleware( + errorLimiter.Limit("post_device_trigger_linux_escrow", desktopQuota), + ).POST("/api/_version_/fleet/device/{token}/mdm/linux/trigger_escrow", triggerLinuxDiskEncryptionEscrowEndpoint, triggerLinuxDiskEncryptionEscrowRequest{}) + // host-authenticated endpoints he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) @@ -879,6 +883,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC oeWindowsMDM := oe.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM()) oeWindowsMDM.POST("/api/fleet/orbit/disk_encryption_key", postOrbitDiskEncryptionKeyEndpoint, orbitPostDiskEncryptionKeyRequest{}) + oe.POST("/api/fleet/orbit/luks_data", postOrbitLUKSEndpoint, orbitPostLUKSRequest{}) + // unauthenticated endpoints - most of those are either login-related, // invite-related or host-enrolling. So they typically do some kind of // one-time authentication by verifying that a valid secret token is provided diff --git a/server/service/hosts.go b/server/service/hosts.go index 33e32c438036..b2ee905999f7 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -2200,12 +2200,51 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host return nil, err } - // The middleware checks that either Apple or Windows MDM are configured and - // enabled, but here we must check if the specific one is enabled for that - // particular host's platform. + var key *fleet.HostDiskEncryptionKey + if host.IsLUKSSupported() { + if svc.config.Server.PrivateKey == "" { + return nil, ctxerr.Wrap(ctx, errors.New("private key is unavailable"), "getting host encryption key") + } + + key, err = svc.ds.GetHostDiskEncryptionKey(ctx, id) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") + } + if key.Base64Encrypted == "" { + return nil, ctxerr.Wrap(ctx, newNotFoundError(), "host encryption key is not set") + } + + decryptedKey, err := mdm.DecodeAndDecrypt(key.Base64Encrypted, svc.config.Server.PrivateKey) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key") + } + key.DecryptedValue = decryptedKey + } else { + key, err = svc.decryptForMDMPlatform(ctx, host) + if err != nil { + return nil, err + } + } + + err = svc.NewActivity( + ctx, + authz.UserFromContext(ctx), + fleet.ActivityTypeReadHostDiskEncryptionKey{ + HostID: host.ID, + HostDisplayName: host.DisplayName(), + }, + ) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity") + } + + return key, nil +} + +func (svc *Service) decryptForMDMPlatform(ctx context.Context, host *fleet.Host) (*fleet.HostDiskEncryptionKey, error) { + // Here we must check if the appropriate MDM is enabled for that particular host's platform. var decryptCert *tls.Certificate - switch host.FleetPlatform() { - case "windows": + if host.FleetPlatform() == "windows" { if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil { return nil, err } @@ -2216,8 +2255,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host return nil, ctxerr.Wrap(ctx, err, "getting Microsoft WSTEP certificate to decrypt key") } decryptCert = cert - - default: + } else { if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { return nil, err } @@ -2230,7 +2268,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host decryptCert = cert } - key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id) + key, err := svc.ds.GetHostDiskEncryptionKey(ctx, host.ID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") } @@ -2242,20 +2280,8 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host if err != nil { return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key") } - key.DecryptedValue = string(decryptedKey) - - err = svc.NewActivity( - ctx, - authz.UserFromContext(ctx), - fleet.ActivityTypeReadHostDiskEncryptionKey{ - HostID: host.ID, - HostDisplayName: host.DisplayName(), - }, - ) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity") - } + key.DecryptedValue = string(decryptedKey) return key, nil } diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index 5b0837cf60c2..ebca688f3ff6 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -19,6 +19,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki" @@ -1448,6 +1449,73 @@ func TestHostEncryptionKey(t *testing.T) { }) } }) + + t.Run("Linux encryption", func(t *testing.T) { + ds := new(mock.Store) + host := &fleet.Host{ID: 1, Platform: "ubuntu"} + symmetricKey := "this_is_a_32_byte_symmetric_key!" + passphrase := "this_is_a_passphrase" + base64EncryptedKey, err := mdm.EncryptAndEncode(passphrase, symmetricKey) + require.NoError(t, err) + + ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { + return host, nil + } + + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { + return nil + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { // needed for new activity + return &fleet.AppConfig{}, nil + } + + // error when no server private key + fleetCfg.Server.PrivateKey = "" + svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err := svc.HostEncryptionKey(ctx, 1) + require.Error(t, err, "private key is unavailable") + require.Nil(t, key) + + // error when key is not set + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{}, nil + } + fleetCfg.Server.PrivateKey = symmetricKey + svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err = svc.HostEncryptionKey(ctx, 1) + require.Error(t, err, "host encryption key is not set") + require.Nil(t, key) + + // error when key is not set + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{ + Base64Encrypted: "thisIsWrong", + Decryptable: ptr.Bool(true), + }, nil + } + svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err = svc.HostEncryptionKey(ctx, 1) + require.Error(t, err, "decrypt host encryption key") + require.Nil(t, key) + + // happy path + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{ + Base64Encrypted: base64EncryptedKey, + Decryptable: ptr.Bool(true), + }, nil + } + svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err = svc.HostEncryptionKey(ctx, 1) + require.NoError(t, err) + require.Equal(t, passphrase, key.DecryptedValue) + }) } func TestHostMDMProfileDetail(t *testing.T) { diff --git a/server/service/orbit.go b/server/service/orbit.go index be10578dc5e6..e68bc657f825 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -16,6 +16,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/worker" @@ -287,6 +288,9 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro } } + notifs.RunDiskEncryptionEscrow = host.IsLUKSSupported() && + host.DiskEncryptionEnabled != nil && *host.DiskEncryptionEnabled && svc.ds.IsHostPendingEscrow(ctx, host.ID) + pendingInstalls, err := svc.ds.ListPendingSoftwareInstalls(ctx, host.ID) if err != nil { return fleet.OrbitConfig{}, err @@ -368,6 +372,11 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro updateChannels = &uc } + // only unset this flag once we know there were no errors so this notification will be picked up by the agent + if notifs.RunDiskEncryptionEscrow { + _ = svc.ds.ClearPendingEscrow(ctx, host.ID) + } + return fleet.OrbitConfig{ ScriptExeTimeout: opts.ScriptExecutionTimeout, Flags: opts.CommandLineStartUpFlags, @@ -438,6 +447,11 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro updateChannels = &uc } + // only unset this flag once we know there were no errors so this notification will be picked up by the agent + if notifs.RunDiskEncryptionEscrow { + _ = svc.ds.ClearPendingEscrow(ctx, host.ID) + } + return fleet.OrbitConfig{ ScriptExeTimeout: opts.ScriptExecutionTimeout, Flags: opts.CommandLineStartUpFlags, @@ -1004,6 +1018,85 @@ func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryption return nil } +///////////////////////////////////////////////////////////////////////////////// +// Post Orbit LUKS (Linux disk encryption) data +///////////////////////////////////////////////////////////////////////////////// + +type orbitPostLUKSRequest struct { + OrbitNodeKey string `json:"orbit_node_key"` + Passphrase string `json:"passphrase"` + Salt string `json:"salt"` + KeySlot *uint `json:"key_slot"` + ClientError string `json:"client_error"` +} + +// interface implementation required by the OrbitClient +func (r *orbitPostLUKSRequest) setOrbitNodeKey(nodeKey string) { + r.OrbitNodeKey = nodeKey +} + +// interface implementation required by orbit authentication +func (r *orbitPostLUKSRequest) orbitHostNodeKey() string { + return r.OrbitNodeKey +} + +type orbitPostLUKSResponse struct { + Err error `json:"error,omitempty"` +} + +func (r orbitPostLUKSResponse) error() error { return r.Err } +func (r orbitPostLUKSResponse) Status() int { return http.StatusNoContent } + +func postOrbitLUKSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*orbitPostLUKSRequest) + if err := svc.EscrowLUKSData(ctx, req.Passphrase, req.Salt, req.KeySlot, req.ClientError); err != nil { + return orbitPostLUKSResponse{Err: err}, nil + } + return orbitPostLUKSResponse{}, nil +} + +func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, salt string, keySlot *uint, clientError string) error { + // this is not a user-authenticated endpoint + svc.authz.SkipAuthorization(ctx) + + host, ok := hostctx.FromContext(ctx) + if !ok { + return newOsqueryError("internal error: missing host from request context") + } + + if clientError != "" { + return svc.ds.ReportEscrowError(ctx, host.ID, clientError) + } + + encryptedPassphrase, encryptedSalt, validatedKeySlot, err := svc.validateAndEncrypt(ctx, passphrase, salt, keySlot) + if err != nil { + _ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error()) + return err + } + + return svc.ds.SaveLUKSData(ctx, host.ID, encryptedPassphrase, encryptedSalt, validatedKeySlot) +} + +func (svc *Service) validateAndEncrypt(ctx context.Context, passphrase string, salt string, keySlot *uint) (encryptedPassphrase string, encryptedSalt string, validatedKeySlot uint, err error) { + if passphrase == "" || salt == "" || keySlot == nil { + return "", "", 0, badRequest("passphrase, salt, and key_slot must be provided to escrow LUKS data") + } + if svc.config.Server.PrivateKey == "" { + return "", "", 0, newOsqueryError("internal error: missing server private key") + } + + encryptedPassphrase, err = mdm.EncryptAndEncode(passphrase, svc.config.Server.PrivateKey) + if err != nil { + return "", "", 0, ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data") + } + encryptedSalt, err = mdm.EncryptAndEncode(salt, svc.config.Server.PrivateKey) + if err != nil { + return "", "", 0, ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data") + } + + return encryptedPassphrase, encryptedSalt, *keySlot, nil +} + ///////////////////////////////////////////////////////////////////////////////// // Get Orbit pending software installations ///////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/orbit_test.go b/server/service/orbit_test.go index 992d8b057c70..3dc98b64aa95 100644 --- a/server/service/orbit_test.go +++ b/server/service/orbit_test.go @@ -4,16 +4,268 @@ import ( "context" "database/sql" "encoding/json" + "errors" "testing" "github.com/fleetdm/fleet/v4/pkg/optjson" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/require" ) +func TestGetOrbitConfigLinuxEscrow(t *testing.T) { + t.Run("don't check for pending escrow if unsupported platform or encryption is not enabled", 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: "rhel", + Version: "9.0", + } + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + OSVersion: "Red Hat Enterprise Linux 9.0", + Platform: "rhel", + } + + team := fleet.Team{ID: 1} + teamMDM := fleet.TeamMDM{EnableDiskEncryption: true} + 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 + } + ds.ListPendingSoftwareInstallsFunc = func(ctx context.Context, hostID uint) ([]string, error) { + return nil, nil + } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return true, nil + } + ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { + return nil, nil + } + + appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appCfg, nil + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } + + ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) { + return false, nil + } + + ctx = test.HostContext(ctx, host) + + cfg, err := svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.False(t, cfg.Notifications.RunDiskEncryptionEscrow) + + host.OSVersion = "Fedora 38.0" + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.False(t, cfg.Notifications.RunDiskEncryptionEscrow) + }) + + t.Run("pending escrow sets config flag and clears in DB", 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: "ubuntu", + Version: "20.04", + } + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + OSVersion: "Ubuntu 20.04", + Platform: "ubuntu", + DiskEncryptionEnabled: ptr.Bool(true), + } + + team := fleet.Team{ID: 1} + teamMDM := fleet.TeamMDM{EnableDiskEncryption: true} + 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 + } + ds.ListPendingSoftwareInstallsFunc = func(ctx context.Context, hostID uint) ([]string, error) { + return nil, nil + } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return true, nil + } + ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { + return nil, nil + } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return true + } + ds.ClearPendingEscrowFunc = func(ctx context.Context, hostID uint) error { + return nil + } + + appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appCfg, nil + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } + + ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) { + return false, nil + } + + ctx = test.HostContext(ctx, host) + + // no-team + cfg, err := svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.True(t, cfg.Notifications.RunDiskEncryptionEscrow) + require.True(t, ds.ClearPendingEscrowFuncInvoked) + + // with team + ds.ClearPendingEscrowFuncInvoked = false + host.TeamID = ptr.Uint(team.ID) + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.True(t, cfg.Notifications.RunDiskEncryptionEscrow) + require.True(t, ds.ClearPendingEscrowFuncInvoked) + + // ignore clear escrow errors + ds.ClearPendingEscrowFuncInvoked = false + ds.ClearPendingEscrowFunc = func(ctx context.Context, hostID uint) error { + return errors.New("clear pending escrow") + } + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.True(t, cfg.Notifications.RunDiskEncryptionEscrow) + require.True(t, ds.ClearPendingEscrowFuncInvoked) + }) +} + +func TestOrbitLUKSDataSave(t *testing.T) { + t.Run("when private key is set", 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}) + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + } + ctx = test.HostContext(ctx, host) + expectedErrorMessage := "There was an error." + ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error { + require.Equal(t, expectedErrorMessage, err) + return nil + } + + // test reporting client errors + err := svc.EscrowLUKSData(ctx, "foo", "bar", nil, expectedErrorMessage) + require.NoError(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + + // blank passphrase + ds.ReportEscrowErrorFuncInvoked = false + expectedErrorMessage = "passphrase, salt, and key_slot must be provided to escrow LUKS data" + err = svc.EscrowLUKSData(ctx, "", "bar", ptr.Uint(0), "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + + ds.ReportEscrowErrorFuncInvoked = false + passphrase, salt := "foo", "" + var keySlot *uint + ds.SaveLUKSDataFunc = func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlotToPersist uint) error { + require.Equal(t, host.ID, hostID) + key := config.TestConfig().Server.PrivateKey + + decryptedPassphrase, err := mdm.DecodeAndDecrypt(encryptedBase64Passphrase, key) + require.NoError(t, err) + require.Equal(t, passphrase, decryptedPassphrase) + + decryptedSalt, err := mdm.DecodeAndDecrypt(encryptedBase64Salt, key) + require.NoError(t, err) + require.Equal(t, salt, decryptedSalt) + + require.Equal(t, *keySlot, keySlotToPersist) + + return nil + } + + // with no salt + err = svc.EscrowLUKSData(ctx, passphrase, salt, keySlot, "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + require.False(t, ds.SaveLUKSDataFuncInvoked) + + // with no key slot + ds.ReportEscrowErrorFuncInvoked = false + salt = "baz" + err = svc.EscrowLUKSData(ctx, passphrase, salt, keySlot, "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + require.False(t, ds.SaveLUKSDataFuncInvoked) + + // with salt and key slot + keySlot = ptr.Uint(0) + ds.ReportEscrowErrorFuncInvoked = false + err = svc.EscrowLUKSData(ctx, passphrase, salt, keySlot, "") + require.NoError(t, err) + require.False(t, ds.ReportEscrowErrorFuncInvoked) + require.True(t, ds.SaveLUKSDataFuncInvoked) + }) + + t.Run("fail when no/invalid private key is set", func(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + } + expectedErrorMessage := "internal error: missing server private key" + ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error { + require.Equal(t, expectedErrorMessage, err) + return nil + } + + cfg := config.TestConfig() + cfg.Server.PrivateKey = "" + svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + ctx = test.HostContext(ctx, host) + err := svc.EscrowLUKSData(ctx, "foo", "bar", ptr.Uint(0), "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + + expectedErrorMessage = "internal error: could not encrypt LUKS data: create new cipher: crypto/aes: invalid key size 7" + ds.ReportEscrowErrorFuncInvoked = false + cfg.Server.PrivateKey = "invalid" + svc, ctx = newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + ctx = test.HostContext(ctx, host) + err = svc.EscrowLUKSData(ctx, "foo", "bar", ptr.Uint(0), "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + }) +} + func TestGetOrbitConfigNudge(t *testing.T) { t.Run("missing values in AppConfig", func(t *testing.T) { ds := new(mock.Store) @@ -39,6 +291,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { return true, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { return &fleet.HostMDM{ @@ -114,6 +369,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { return true, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { return &fleet.HostMDM{ @@ -161,7 +419,7 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.TeamMDMConfigFuncInvoked = false }) - t.Run("non-elegible MDM status", func(t *testing.T) { + t.Run("non-eligible MDM status", 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}) @@ -207,6 +465,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) { return false, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } checkEmptyNudgeConfig := func(h *fleet.Host) { ctx := test.HostContext(ctx, h) @@ -283,6 +544,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { Name: fleet.WellKnownMDMFleet, }, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}} appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") @@ -315,6 +579,7 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.GetHostOperatingSystemFuncInvoked = false cfg, err = svc.GetOrbitConfig(ctx) require.NoError(t, err) + require.False(t, cfg.Notifications.RunDiskEncryptionEscrow) require.Empty(t, cfg.NudgeConfig) require.True(t, ds.GetHostOperatingSystemFuncInvoked) From 65a175fa281c938e39cd04520384938e49874663 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:20:57 -0800 Subject: [PATCH 15/36] =?UTF-8?q?to=20RC:=20Linux=20disk=20encryption:=20f?= =?UTF-8?q?rontend=20changes,=20backend=20missing=20private=20key=20?= =?UTF-8?q?=E2=80=A6=20(#23983)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### This PR already merged to `main`, see https://github.com/fleetdm/fleet/pull/23714. This is against the release branch so it can be included in 4.60.0. Co-authored-by: Jacob Shandling Co-authored-by: Ian Littman --- changes/22702-linux-encryption-frontend | 1 + cmd/fleetctl/apply_test.go | 4 +- ee/server/service/mdm_external_test.go | 3 + ee/server/service/teams.go | 18 ++- frontend/components/InfoBanner/InfoBanner.tsx | 3 +- .../SectionHeader/SectionHeader.tsx | 24 ++-- .../components/SectionHeader/_styles.scss | 9 ++ frontend/interfaces/mdm.ts | 20 +++- frontend/interfaces/platform.ts | 56 +++++++++- .../OSSettings/OSSettings.tsx | 11 +- .../cards/CustomSettings/CustomSettings.tsx | 17 ++- .../cards/CustomSettings/_styles.scss | 15 ++- .../cards/DiskEncryption/DiskEncryption.tsx | 81 +++++++++++--- .../cards/DiskEncryption/_styles.scss | 10 +- .../DiskEncryptionTable.tsx | 6 +- .../DiskEncryptionTableConfig.tsx | 18 ++- .../CurrentVersionSection.tsx | 2 +- .../TargetSection/TargetSection.tsx | 5 +- .../SettingsSection/SettingsSection.tsx | 2 +- .../CreateLinuxKeyModal.tsx | 57 ++++++++++ .../CreateLinuxKeyModal/_styles.scss | 8 ++ .../CreateLinuxKeyModal/index.ts | 1 + .../details/DeviceUserPage/DeviceUserPage.tsx | 41 ++++++- .../DeviceUserBanners.tests.tsx | 69 ++++++++++-- .../DeviceUserBanners/DeviceUserBanners.tsx | 87 ++++++++++++--- .../components/DeviceUserBanners/_styles.scss | 6 + .../HostDetailsPage/HostDetailsPage.tsx | 34 +++--- .../HostDetailsBanners/HostDetailsBanners.tsx | 91 +++++++++++----- .../DiskEncryptionKeyModal.tsx | 46 +++----- .../OSSettingsModal/OSSettingsModal.tsx | 3 +- .../OSSettingStatusCell.tsx | 20 +++- .../OSSettingStatusCell/helpers.ts | 24 ++++ .../OSSettingsTable/OSSettingsTableConfig.tsx | 40 ++++++- .../OSSettingsTable/_styles.scss | 24 ++-- .../details/cards/HostSummary/HostSummary.tsx | 82 +++++++++----- .../OSSettingsIndicator.tsx | 2 +- frontend/pages/hosts/details/helpers.ts | 35 ++++-- frontend/services/entities/disk_encryption.ts | 60 ++++++++++ frontend/services/entities/mdm.ts | 42 ------- frontend/utilities/endpoints.ts | 6 + server/fleet/service.go | 4 +- server/service/appconfig.go | 12 +- server/service/appconfig_test.go | 53 +++++++++ server/service/apple_mdm_test.go | 6 +- server/service/handler.go | 10 +- server/service/integration_core_test.go | 6 +- server/service/integration_mdm_test.go | 103 ++++++------------ server/service/mdm.go | 8 +- server/service/testing_utils.go | 7 -- 49 files changed, 921 insertions(+), 371 deletions(-) create mode 100644 changes/22702-linux-encryption-frontend create mode 100644 frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/CreateLinuxKeyModal.tsx create mode 100644 frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/_styles.scss create mode 100644 frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/index.ts create mode 100644 frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss create mode 100644 frontend/services/entities/disk_encryption.ts diff --git a/changes/22702-linux-encryption-frontend b/changes/22702-linux-encryption-frontend new file mode 100644 index 000000000000..a35d2423751b --- /dev/null +++ b/changes/22702-linux-encryption-frontend @@ -0,0 +1 @@ +- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index ba5ba641f754..3c2bbee35640 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -3781,7 +3781,9 @@ spec: macos_settings: enable_disk_encryption: true `, - wantErr: `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on`, + + // Since Linux disk encryption does not use MDM, we allow enabling it even without MDM enabled and configured + wantOutput: `[+] applied fleet config`, }, { desc: "app config macos_settings.enable_disk_encryption false", diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index 9cb9aa46f0cf..71272b08af65 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -45,6 +45,9 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser AppleSCEPCertBytes: eeservice.TestCert, AppleSCEPKeyBytes: eeservice.TestKey, }, + Server: config.ServerConfig{ + PrivateKey: "foo", + }, } depStorage := &nanodep_mock.Storage{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 34d3b1f76c2c..20a526197bf5 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1046,13 +1046,9 @@ func (svc *Service) createTeamFromSpec( } invalid := &fleet.InvalidArgumentError{} - if enableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() { - invalid.Append( - "mdm", - `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`, - ) + if enableDiskEncryption && svc.config.Server.PrivateKey == "" { + return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } - validateTeamCustomSettings(invalid, "macos", macOSSettings.CustomSettings) validateTeamCustomSettings(invalid, "windows", spec.MDM.WindowsSettings.CustomSettings.Value) @@ -1210,11 +1206,10 @@ func (svc *Service) editTeamFromSpec( team.Config.MDM.EnableDiskEncryption = *de } didUpdateDiskEncryption := team.Config.MDM.EnableDiskEncryption != oldEnableDiskEncryption - if !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() && didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", - `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)) - } + if didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption && svc.config.Server.PrivateKey == "" { + return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } if !team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid { team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false) } @@ -1523,6 +1518,9 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) { func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.Team, enable *bool) error { var didUpdate, didUpdateMacOSDiskEncryption bool if enable != nil { + if svc.config.Server.PrivateKey == "" { + return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } if tm.Config.MDM.EnableDiskEncryption != *enable { tm.Config.MDM.EnableDiskEncryption = *enable didUpdate = true diff --git a/frontend/components/InfoBanner/InfoBanner.tsx b/frontend/components/InfoBanner/InfoBanner.tsx index 1a78e0a486d3..bd49e3d20645 100644 --- a/frontend/components/InfoBanner/InfoBanner.tsx +++ b/frontend/components/InfoBanner/InfoBanner.tsx @@ -15,10 +15,11 @@ export interface IInfoBannerProps { /** default 4px */ borderRadius?: "large" | "xlarge"; pageLevel?: boolean; - /** cta and link are mutually exclusive */ + /** Add this element to the end of the banner message. Mutually exclusive with `link`. */ cta?: JSX.Element; /** closable and link are mutually exclusive */ closable?: boolean; + /** Makes the entire banner clickable */ link?: string; icon?: IconNames; } diff --git a/frontend/components/SectionHeader/SectionHeader.tsx b/frontend/components/SectionHeader/SectionHeader.tsx index f540ae821cce..c3ebe5e76888 100644 --- a/frontend/components/SectionHeader/SectionHeader.tsx +++ b/frontend/components/SectionHeader/SectionHeader.tsx @@ -7,24 +7,32 @@ interface ISectionHeaderProps { title: string; subTitle?: React.ReactNode; details?: JSX.Element; - className?: string; + wrapperCustomClass?: string; + alignLeftHeaderVertically?: boolean; + greySubtitle?: boolean; } const SectionHeader = ({ title, subTitle, details, - className, + wrapperCustomClass, + alignLeftHeaderVertically, + greySubtitle, }: ISectionHeaderProps) => { - const classNames = classnames(baseClass, className); + const wrapperClassnames = classnames(baseClass, wrapperCustomClass); + const leftHeaderClassnames = classnames(`${baseClass}__left-header`, { + [`${baseClass}__left-header--vertical`]: alignLeftHeaderVertically, + }); + const subTitleClassnames = classnames(`${baseClass}__sub-title`, { + [`${baseClass}__sub-title--grey`]: greySubtitle, + }); return ( -
-
+
+

{title}

- {subTitle && ( -
{subTitle}
- )} + {subTitle &&
{subTitle}
}
{details &&
{details}
}
diff --git a/frontend/components/SectionHeader/_styles.scss b/frontend/components/SectionHeader/_styles.scss index c9f1b6412ecb..943ea5c9e163 100644 --- a/frontend/components/SectionHeader/_styles.scss +++ b/frontend/components/SectionHeader/_styles.scss @@ -7,6 +7,15 @@ display: flex; align-items: center; gap: $pad-small; + &--vertical { + flex-direction: column; + } + } + + &__sub-title { + &--grey { + @include grey-text; + } } h2 { diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 07fe2cffb97c..fba6866e3344 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -1,5 +1,4 @@ import { IConfigServerSettings } from "./config"; -import { ITeamSummary } from "./team"; export interface IMdmApple { common_name: string; @@ -93,7 +92,7 @@ export interface IMdmSummaryResponse { mobile_device_management_solution: IMdmSummaryMdmSolution[] | null; } -export type ProfilePlatform = "darwin" | "windows" | "ios" | "ipados"; +export type ProfilePlatform = "darwin" | "windows" | "ios" | "ipados" | "linux"; export interface IProfileLabel { name: string; @@ -129,10 +128,11 @@ export interface IHostMdmProfile { name: string; operation_type: ProfileOperationType | null; platform: ProfilePlatform; - status: MdmProfileStatus | MdmDDMProfileStatus; + status: MdmProfileStatus | MdmDDMProfileStatus | LinuxDiskEncryptionStatus; detail: string; } +// TODO - move disk encryption related types to dedicated file export type DiskEncryptionStatus = | "verified" | "verifying" @@ -143,14 +143,14 @@ export type DiskEncryptionStatus = /** Currently windows disk enxryption status will only be one of these four values. In the future we may add more. */ -export type IWindowsDiskEncryptionStatus = Extract< +export type WindowsDiskEncryptionStatus = Extract< DiskEncryptionStatus, "verified" | "verifying" | "enforcing" | "failed" >; export const isWindowsDiskEncryptionStatus = ( status: DiskEncryptionStatus -): status is IWindowsDiskEncryptionStatus => { +): status is WindowsDiskEncryptionStatus => { switch (status) { case "verified": case "verifying": @@ -162,6 +162,16 @@ export const isWindowsDiskEncryptionStatus = ( } }; +export type LinuxDiskEncryptionStatus = Extract< + DiskEncryptionStatus, + "verified" | "failed" | "action_required" +>; + +export const isLinuxDiskEncryptionStatus = ( + status: DiskEncryptionStatus +): status is LinuxDiskEncryptionStatus => + ["verified", "failed", "action_required"].includes(status); + export const FLEET_FILEVAULT_PROFILE_DISPLAY_NAME = "Disk encryption"; export interface IMdmSSOReponse { diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts index 2be1f412d4b3..1f33a1639fdf 100644 --- a/frontend/interfaces/platform.ts +++ b/frontend/interfaces/platform.ts @@ -64,9 +64,9 @@ export const MACADMINS_EXTENSION_TABLES: Record = { */ export const HOST_LINUX_PLATFORMS = [ "linux", - "ubuntu", + "ubuntu", // covers Kubuntu "debian", - "rhel", + "rhel", // covers Fedora "centos", "sles", "kali", @@ -111,3 +111,55 @@ export const isAppleDevice = (platform: string) => { export const isIPadOrIPhone = (platform: string | HostPlatform) => ["ios", "ipados"].includes(platform); + +export const DISK_ENCRYPTION_SUPPORTED_LINUX_PLATFORMS = [ + "ubuntu", // covers Kubuntu + "rhel", // *included here to support Fedora systems. Necessary to cross-check with `os_versions` as well to confrim host is Fedora and not another, non-support rhel-like platform. +] as const; + +export const isDiskEncryptionSupportedLinuxPlatform = ( + platform: HostPlatform, + os_version: string +) => { + const isFedora = + platform === "rhel" && os_version.toLowerCase().includes("fedora"); + return isFedora || platform === "ubuntu"; +}; + +const DISK_ENCRYPTION_SUPPORTED_PLATFORMS = [ + "darwin", + "windows", + "chrome", + ...DISK_ENCRYPTION_SUPPORTED_LINUX_PLATFORMS, +] as const; + +export type DiskEncryptionSupportedPlatform = typeof DISK_ENCRYPTION_SUPPORTED_PLATFORMS[number]; + +export const platformSupportsDiskEncryption = ( + platform: HostPlatform, + /** os_version necessary to differentiate Fedora from other rhel-like platforms */ + os_version?: string +) => { + if (platform === "rhel") { + return !!os_version && os_version.toLowerCase().includes("fedora"); + } + return DISK_ENCRYPTION_SUPPORTED_PLATFORMS.includes( + platform as DiskEncryptionSupportedPlatform + ); +}; + +const OS_SETTINGS_DISPLAY_PLATFORMS = [ + ...DISK_ENCRYPTION_SUPPORTED_PLATFORMS, + "ios", + "ipados", +]; + +export const isOsSettingsDisplayPlatform = ( + platform: HostPlatform, + os_version: string +) => { + if (platform === "rhel") { + return !!os_version && os_version.toLowerCase().includes("fedora"); + } + return OS_SETTINGS_DISPLAY_PLATFORMS.includes(platform); +}; diff --git a/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx index f6cf8c435f67..dff1dc41dffc 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx @@ -9,7 +9,6 @@ import mdmAPI from "services/entities/mdm"; import OS_SETTINGS_NAV_ITEMS from "./OSSettingsNavItems"; import ProfileStatusAggregate from "./ProfileStatusAggregate"; -import TurnOnMdmMessage from "../../../components/TurnOnMdmMessage"; const baseClass = "os-settings"; @@ -29,7 +28,7 @@ const OSSettings = ({ params, }: IOSSettingsProps) => { const { section } = params; - const { config, currentTeam } = useContext(AppContext); + const { currentTeam } = useContext(AppContext); // TODO: consider using useTeamIdParam hook here instead in the future const teamId = @@ -51,14 +50,6 @@ const OSSettings = ({ } ); - // MDM is not on so show messaging for user to enable it. - if ( - !config?.mdm.enabled_and_configured && - !config?.mdm.windows_enabled_and_configured - ) { - return ; - } - const DEFAULT_SETTINGS_SECTION = OS_SETTINGS_NAV_ITEMS[0]; const currentFormSection = diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx index ede9ac75b1ff..b0e31bf3b201 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx @@ -14,6 +14,7 @@ import CustomLink from "components/CustomLink"; import SectionHeader from "components/SectionHeader"; import Spinner from "components/Spinner"; import DataError from "components/DataError"; +import TurnOnMdmMessage from "components/TurnOnMdmMessage"; import Pagination from "pages/ManageControlsPage/components/Pagination"; @@ -46,7 +47,11 @@ const CustomSettings = ({ onMutation, }: ICustomSettingsProps) => { const { renderFlash } = useContext(NotificationContext); - const { isPremiumTier } = useContext(AppContext); + const { config, isPremiumTier } = useContext(AppContext); + + const mdmEnabled = + config?.mdm.enabled_and_configured || + config?.mdm.windows_enabled_and_configured; const [showAddProfileModal, setShowAddProfileModal] = useState(false); const [ @@ -78,6 +83,7 @@ const CustomSettings = ({ per_page: PROFILES_PER_PAGE, }), { + enabled: mdmEnabled, refetchOnWindowFocus: false, } ); @@ -185,7 +191,14 @@ const CustomSettings = ({ url="https://fleetdm.com/learn-more-about/custom-os-settings" />

- <>{renderProfileList()} + {!mdmEnabled ? ( + + ) : ( + renderProfileList() + )} {showAddProfileModal && ( { try { - await mdmAPI.updateAppleMdmSettings(diskEncryptionEnabled, currentTeamId); + await diskEncryptionAPI.updateDiskEncryption( + diskEncryptionEnabled, + currentTeamId + ); renderFlash( "success", "Successfully updated disk encryption enforcement!" @@ -91,11 +99,24 @@ const DiskEncryption = ({ if (currentTeamId === 0) { getUpdatedAppConfig(); } - } catch { - renderFlash( - "error", - "Could not update the disk encryption enforcement. Please try again." - ); + } catch (e) { + if (getErrorReason(e).includes("Missing required private key")) { + const link = + "https://fleetdm.com/learn-more-about/fleet-server-private-key"; + renderFlash( + "error", + <> + Could't enable disk encryption. Missing required private key. + Learn how to configure the private key here:{" "} + {link} + + ); + } else { + renderFlash( + "error", + "Could not update the disk encryption enforcement. Please try again." + ); + } } }; @@ -103,18 +124,43 @@ const DiskEncryption = ({ setIsLoadingTeam(false); } - const createDescriptionText = () => { - // table is showing disk encryption status. - if (showAggregate) { - return "If turned on, hosts' disk encryption keys will be stored in Fleet. "; - } - - return `Also known as “FileVault” on macOS and “BitLocker” on Windows. If turned on, hosts' disk encryption keys will be stored in Fleet. `; + const getTipContent = (platform: "windows" | "macOS") => { + const [AppleOrWindows, DEMethod] = + platform === "windows" + ? ["Windows", "BitLocker"] + : ["Apple", "FileVault"]; + return ( + <> + {AppleOrWindows} MDM must be turned on in{" "} + + Settings > Integrations >{" "} + Mobile Device Management (MDM) + {" "} + to enforce disk encryption via {DEMethod}. + + ); }; + const subTitle = ( + <> + Disk encryption is available on{" "} + macOS + ,{" "} + + Windows + + , Ubuntu Linux, and Fedora Linux hosts. + + ); + return (
- + {!isPremiumTier ? (

- {createDescriptionText()} + If turned on, hosts' disk encryption keys will be stored in + Fleet{" "}

diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/_styles.scss index 761a1f0e9efe..c79883ca8527 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/_styles.scss @@ -1,7 +1,7 @@ .disk-encryption { - &__premium-feature-message { - margin-top: 80px; - text-align: center; + .premium-feature-message-container { + justify-content: initial; + align-items: initial; } > p { @@ -15,4 +15,8 @@ .disk-encryption-content { animation: fade-in 250ms ease-out; } + .section-header__sub-title a { + font-size: inherit; + color: inherit; + } } diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx index 75391fa7f5c3..ae267d3ad1d6 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx @@ -7,7 +7,9 @@ import PATHS from "router/paths"; import { buildQueryStringFromParams } from "utilities/url"; -import mdmAPI, { IDiskEncryptionSummaryResponse } from "services/entities/mdm"; +import diskEncryptionAPI, { + IDiskEncryptionSummaryResponse, +} from "services/entities/disk_encryption"; import { HOSTS_QUERY_PARAMS } from "services/entities/hosts"; import TableContainer from "components/TableContainer"; @@ -43,7 +45,7 @@ const DiskEncryptionTable = ({ error: diskEncryptionStatusError, } = useQuery( ["disk-encryption-summary", currentTeamId], - () => mdmAPI.getDiskEncryptionSummary(currentTeamId), + () => diskEncryptionAPI.getDiskEncryptionSummary(currentTeamId), { refetchOnWindowFocus: false, retry: false, diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx index 1eddba30ad07..6930b1cd5919 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx @@ -4,7 +4,7 @@ import { DiskEncryptionStatus } from "interfaces/mdm"; import { IDiskEncryptionStatusAggregate, IDiskEncryptionSummaryResponse, -} from "services/entities/mdm"; +} from "services/entities/disk_encryption"; import TextCell from "components/TableContainer/DataTable/TextCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -108,6 +108,21 @@ const defaultTableHeaders: IDataColumn[] = [ return ; }, }, + { + title: "Linux hosts", + Header: (cellProps: IHeaderProps) => ( + + ), + disableSortBy: true, + accessor: "linuxHosts", + Cell: ({ cell: { value: aggregateCount } }: ICellProps) => { + return ; + }, + }, { title: "", Header: "", @@ -200,6 +215,7 @@ export const generateTableData = ( status: STATUS_CELL_VALUES[status], macosHosts: statusAggregate.macos, windowsHosts: statusAggregate.windows, + linuxHosts: statusAggregate.linux, teamId: currentTeamId, }); diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx index a8305675c1a7..7e1cc7f085d5 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx @@ -136,7 +136,7 @@ const CurrentVersionSection = ({ {renderTable()}
diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/TargetSection/TargetSection.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/TargetSection/TargetSection.tsx index 0bc2f80c91d9..b0bb07e3c114 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/TargetSection/TargetSection.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/TargetSection/TargetSection.tsx @@ -194,7 +194,10 @@ const TargetSection = ({ return (
- + {renderTargetForms()}
); diff --git a/frontend/pages/admin/components/SettingsSection/SettingsSection.tsx b/frontend/pages/admin/components/SettingsSection/SettingsSection.tsx index d3a93ab09b8d..2c692ec2243f 100644 --- a/frontend/pages/admin/components/SettingsSection/SettingsSection.tsx +++ b/frontend/pages/admin/components/SettingsSection/SettingsSection.tsx @@ -21,7 +21,7 @@ const SettingsSection = ({ return (
- + <>{children}
); diff --git a/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/CreateLinuxKeyModal.tsx b/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/CreateLinuxKeyModal.tsx new file mode 100644 index 000000000000..b6e593acdc05 --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/CreateLinuxKeyModal.tsx @@ -0,0 +1,57 @@ +import Button from "components/buttons/Button"; +import Modal from "components/Modal"; +import React from "react"; + +const baseClass = "create-linux-key-modal"; + +interface ICreateLinuxKeyModal { + isTriggeringCreateLinuxKey: boolean; + onExit: () => void; +} + +const CreateLinuxKeyModal = ({ + isTriggeringCreateLinuxKey, + onExit, +}: ICreateLinuxKeyModal) => { + const renderModalBody = () => ( + <> +
    +
  1. + Wait 30 seconds for the Enter disk encryption passphrase pop-up + to open. +
  2. +
  3. + In the pop-up, enter the passphrase used to encrypt your device during + setup. +
  4. +
  5. + Close this window and select Refetch on your My device{" "} + page. This shares the new key with your organization. +
  6. +
+
+ +
+ + ); + return ( + + {renderModalBody()} + + ); +}; + +export default CreateLinuxKeyModal; diff --git a/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/_styles.scss b/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/_styles.scss new file mode 100644 index 000000000000..6e35542a9360 --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/_styles.scss @@ -0,0 +1,8 @@ +.create-linux-key-modal { + ol { + display: flex; + flex-direction: column; + gap: $pad-medium; + line-height: $medium; + } +} diff --git a/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/index.ts b/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/index.ts new file mode 100644 index 000000000000..d418b5f07ac5 --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/CreateLinuxKeyModal/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateLinuxKeyModal"; diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 467b40d4cb29..868383d2379b 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -7,6 +7,7 @@ import { pick, findIndex } from "lodash"; import { NotificationContext } from "context/notification"; import deviceUserAPI from "services/entities/device_user"; +import diskEncryptionAPI from "services/entities/disk_encryption"; import { IDeviceMappingResponse, IMacadminsResponse, @@ -46,6 +47,7 @@ import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import AutoEnrollMdmModal from "./AutoEnrollMdmModal"; import ManualEnrollMdmModal from "./ManualEnrollMdmModal"; +import CreateLinuxKeyModal from "./CreateLinuxKeyModal"; import OSSettingsModal from "../OSSettingsModal"; import BootstrapPackageModal from "../HostDetailsPage/modals/BootstrapPackageModal"; import { parseHostSoftwareQueryParams } from "../cards/Software/HostSoftware"; @@ -108,10 +110,14 @@ const DeviceUserPage = ({ const [showBootstrapPackageModal, setShowBootstrapPackageModal] = useState( false ); + const [showCreateLinuxKeyModal, setShowCreateLinuxKeyModal] = useState(false); const [globalConfig, setGlobalConfig] = useState( null ); const [hasSelfService, setSelfService] = useState(false); + const [isTriggeringCreateLinuxKey, setIsTriggeringCreateLinuxKey] = useState( + false + ); const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery( ["deviceMapping", deviceAuthToken], @@ -321,6 +327,22 @@ const DeviceUserPage = ({ ); }; + const onTriggerEscrowLinuxKey = async () => { + setIsTriggeringCreateLinuxKey(true); + // modal opens in loading state + setShowCreateLinuxKeyModal(true); + try { + await diskEncryptionAPI.triggerLinuxDiskEncryptionKeyEscrow( + deviceAuthToken + ); + } catch (e) { + renderFlash("error", "Failed to trigger key creation."); + setShowCreateLinuxKeyModal(false); + } finally { + setIsTriggeringCreateLinuxKey(false); + } + }; + const renderDeviceUserPage = () => { const failingPoliciesCount = host?.issues?.failing_policies_count || 0; @@ -353,22 +375,25 @@ const DeviceUserPage = ({ mdmEnabledAndConfigured={ !!globalConfig?.mdm.enabled_and_configured } - mdmConnectedToFleet={!!host.mdm.connected_to_fleet} - diskEncryptionStatus={ + connectedToFleetMdm={!!host.mdm.connected_to_fleet} + macDiskEncryptionStatus={ host.mdm.macos_settings?.disk_encryption ?? null } diskEncryptionActionRequired={ host.mdm.macos_settings?.action_required ?? null } onTurnOnMdm={toggleEnrollMdmModal} + onTriggerEscrowLinuxKey={onTriggerEscrowLinuxKey} + diskEncryptionOSSetting={host.mdm.os_settings?.disk_encryption} + diskIsEncrypted={host.disk_encryption_enabled} + diskEncryptionKeyAvailable={host.mdm.encryption_key_available} /> setShowBootstrapPackageModal(false)} /> )} + {showCreateLinuxKeyModal && !!host && ( + { + setShowCreateLinuxKeyModal(false); + }} + /> + )}
); }; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx index cd3f28c01bc8..632d9eecaf59 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx @@ -6,7 +6,8 @@ import DeviceUserBanners from "./DeviceUserBanners"; describe("Device User Banners", () => { const turnOnMdmExpcetedText = /Mobile device management \(MDM\) is off\./; - const resetKeyDiskEncryptExpcetedText = /Disk encryption: Log out of your device or restart it to safeguard your data in case your device is lost or stolen\./; + const resetNonLinuxDiskEncryptKeyExpectedText = /Disk encryption: Log out of your device or restart it to safeguard your data in case your device is lost or stolen\./; + const createNewLinuxDiskEncryptKeyExpectedText = /Disk encryption: Create a new disk encryption key\. This lets your organization help you unlock your device if you forget your passphrase\./; it("renders the turn on mdm banner correctly", () => { render( @@ -14,29 +15,74 @@ describe("Device User Banners", () => { hostPlatform="darwin" mdmEnrollmentStatus="Off" mdmEnabledAndConfigured - mdmConnectedToFleet - diskEncryptionStatus={null} + connectedToFleetMdm + macDiskEncryptionStatus={null} diskEncryptionActionRequired={null} onTurnOnMdm={noop} + onTriggerEscrowLinuxKey={noop} /> ); expect(screen.getByText(turnOnMdmExpcetedText)).toBeInTheDocument(); }); - it("renders the reset key for disk encryption banner correctly", () => { + it("renders the reset key for non-linux disk encryption banner correctly", () => { render( ); expect( - screen.getByText(resetKeyDiskEncryptExpcetedText) + screen.getByText(resetNonLinuxDiskEncryptKeyExpectedText) + ).toBeInTheDocument(); + }); + it("renders the create new linux disk encryption key banner correctly for Ubuntu", () => { + render( + + ); + expect( + screen.getByText(createNewLinuxDiskEncryptKeyExpectedText) + ).toBeInTheDocument(); + }); + it("renders the create new linux disk encryption key banner correctly for Fedora", () => { + render( + + ); + expect( + screen.getByText(createNewLinuxDiskEncryptKeyExpectedText) ).toBeInTheDocument(); }); @@ -47,19 +93,20 @@ describe("Device User Banners", () => { hostPlatform="darwin" mdmEnrollmentStatus={null} mdmEnabledAndConfigured={false} - mdmConnectedToFleet={false} - diskEncryptionStatus={null} + connectedToFleetMdm={false} + macDiskEncryptionStatus={null} diskEncryptionActionRequired={null} onTurnOnMdm={noop} + onTriggerEscrowLinuxKey={noop} /> ); expect(screen.queryByText(turnOnMdmExpcetedText)).not.toBeInTheDocument(); expect( - screen.queryByText(resetKeyDiskEncryptExpcetedText) + screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText) ).not.toBeInTheDocument(); expect( - screen.queryByText(resetKeyDiskEncryptExpcetedText) + screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText) ).not.toBeInTheDocument(); }); }); diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx index 286101a026ff..3d3197ba83f5 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx @@ -2,42 +2,45 @@ import React from "react"; import InfoBanner from "components/InfoBanner"; import Button from "components/buttons/Button"; -import { DiskEncryptionStatus, MdmEnrollmentStatus } from "interfaces/mdm"; import { MacDiskEncryptionActionRequired } from "interfaces/host"; +import { IHostBannersBaseProps } from "pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners"; +import CustomLink from "components/CustomLink"; +import { platformSupportsDiskEncryption } from "interfaces/platform"; const baseClass = "device-user-banners"; -interface IDeviceUserBannersProps { - hostPlatform: string; - mdmEnrollmentStatus: MdmEnrollmentStatus | null; +interface IDeviceUserBannersProps extends IHostBannersBaseProps { mdmEnabledAndConfigured: boolean; - mdmConnectedToFleet: boolean; - diskEncryptionStatus: DiskEncryptionStatus | null; diskEncryptionActionRequired: MacDiskEncryptionActionRequired | null; onTurnOnMdm: () => void; + onTriggerEscrowLinuxKey: () => void; } const DeviceUserBanners = ({ hostPlatform, + hostOsVersion, mdmEnrollmentStatus, mdmEnabledAndConfigured, - mdmConnectedToFleet, - diskEncryptionStatus, + connectedToFleetMdm, + macDiskEncryptionStatus, diskEncryptionActionRequired, onTurnOnMdm, + diskEncryptionOSSetting, + diskIsEncrypted, + diskEncryptionKeyAvailable, + onTriggerEscrowLinuxKey, }: IDeviceUserBannersProps) => { const isMdmUnenrolled = mdmEnrollmentStatus === "Off" || mdmEnrollmentStatus === null; - const diskEncryptionBannersEnabled = - mdmEnabledAndConfigured && mdmConnectedToFleet; + const mdmEnabledAndConnected = mdmEnabledAndConfigured && connectedToFleetMdm; - const showTurnOnMdmBanner = + const showTurnOnAppleMdmBanner = hostPlatform === "darwin" && isMdmUnenrolled && mdmEnabledAndConfigured; - const showDiskEncryptionKeyResetRequired = - diskEncryptionBannersEnabled && - diskEncryptionStatus === "action_required" && + const showMacDiskEncryptionKeyResetRequired = + mdmEnabledAndConnected && + macDiskEncryptionStatus === "action_required" && diskEncryptionActionRequired === "rotate_key"; const turnOnMdmButton = ( @@ -47,7 +50,7 @@ const DeviceUserBanners = ({ ); const renderBanner = () => { - if (showTurnOnMdmBanner) { + if (showTurnOnAppleMdmBanner) { return ( Mobile device management (MDM) is off. MDM allows your organization to @@ -58,7 +61,7 @@ const DeviceUserBanners = ({ ); } - if (showDiskEncryptionKeyResetRequired) { + if (showMacDiskEncryptionKeyResetRequired) { return ( Disk encryption: Log out of your device or restart it to safeguard @@ -68,6 +71,58 @@ const DeviceUserBanners = ({ ); } + // setting applies to a supported Linux host + if ( + hostPlatform && + hostPlatform !== "windows" && + platformSupportsDiskEncryption(hostPlatform, hostOsVersion) && + diskEncryptionOSSetting?.status + ) { + // host not in compliance with setting + if (!diskIsEncrypted) { + // banner 1 + return ( + + } + color="yellow" + > + Disk encryption: Follow the instructions in the guide to encrypt + your device. This lets your organization help you unlock your device + if you forget your password. + + ); + } + // host disk is encrypted, so in compliance with the setting + if (!diskEncryptionKeyAvailable) { + // key is not escrowed: banner 3 + return ( + + Create key + + } + color="yellow" + > + Disk encryption: Create a new disk encryption key. This lets your + organization help you unlock your device if you forget your + passphrase. + + ); + } + } + return null; }; diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss new file mode 100644 index 000000000000..a09bdf15e086 --- /dev/null +++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss @@ -0,0 +1,6 @@ +.device-user-banners { + .create-key-button { + color: $core-fleet-black; + font-weight: $bold; + } +} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 8ef7adccff14..8947a18cf43b 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -93,7 +93,6 @@ import OSSettingsModal from "../OSSettingsModal"; import BootstrapPackageModal from "./modals/BootstrapPackageModal"; import ScriptModalGroup from "./modals/ScriptModalGroup"; import SelectQueryModal from "./modals/SelectQueryModal"; -import { isSupportedPlatform } from "./modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal"; import HostDetailsBanners from "./components/HostDetailsBanners"; import { IShowActivityDetailsData } from "../cards/Activity/Activity"; import LockModal from "./modals/LockModal"; @@ -740,11 +739,6 @@ const HostDetailsPage = ({ } }; - // const hostDeviceStatusUIState = getHostDeviceStatusUIState( - // host.mdm.device_status, - // host.mdm.pending_action - // ); - const renderActionDropdown = () => { if (!host) { return null; @@ -851,10 +845,13 @@ const HostDetailsPage = ({ <>
)} - {showDiskEncryptionModal && - host && - isSupportedPlatform(host.platform) && ( - setShowDiskEncryptionModal(false)} - /> - )} + {showDiskEncryptionModal && host && ( + setShowDiskEncryptionModal(false)} + /> + )} {showBootstrapPackageModal && bootstrapPackageData.details && bootstrapPackageData.name && ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx index 656d3f2debb8..188282bab132 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx @@ -2,27 +2,43 @@ import React, { useContext } from "react"; import { AppContext } from "context/app"; import { DiskEncryptionStatus, MdmEnrollmentStatus } from "interfaces/mdm"; -import { hasLicenseExpired, willExpireWithinXDays } from "utilities/helpers"; +import { hasLicenseExpired } from "utilities/helpers"; import InfoBanner from "components/InfoBanner"; +import { IOSSettings } from "interfaces/host"; +import { + HostPlatform, + platformSupportsDiskEncryption, +} from "interfaces/platform"; const baseClass = "host-details-banners"; -interface IHostDetailsBannersProps { - hostMdmEnrollmentStatus?: MdmEnrollmentStatus | null; - hostPlatform?: string; - diskEncryptionStatus: DiskEncryptionStatus | null | undefined; +export interface IHostBannersBaseProps { + macDiskEncryptionStatus: DiskEncryptionStatus | null | undefined; + mdmEnrollmentStatus: MdmEnrollmentStatus | null; connectedToFleetMdm?: boolean; + hostPlatform?: HostPlatform; + // used to identify Fedora hosts, whose platform is "rhel" + hostOsVersion?: string; + /** Disk encryption setting status and detail, if any, that apply to this host (via a team or the "no team" team) */ + diskEncryptionOSSetting?: IOSSettings["disk_encryption"]; + /** Whether or not this host's disk is encrypted */ + diskIsEncrypted?: boolean; + /** Whether or not Fleet has escrowed the host's disk encryption key */ + diskEncryptionKeyAvailable?: boolean; } - /** * Handles the displaying of banners on the host details page */ const HostDetailsBanners = ({ - hostMdmEnrollmentStatus, + mdmEnrollmentStatus, hostPlatform, + hostOsVersion, connectedToFleetMdm, - diskEncryptionStatus, -}: IHostDetailsBannersProps) => { + macDiskEncryptionStatus, + diskEncryptionOSSetting, + diskIsEncrypted, + diskEncryptionKeyAvailable, +}: IHostBannersBaseProps) => { const { config, isPremiumTier, @@ -53,8 +69,7 @@ const HostDetailsBanners = ({ willVppExpire || isFleetLicenseExpired); - const isMdmUnenrolled = - hostMdmEnrollmentStatus === "Off" || !hostMdmEnrollmentStatus; + const isMdmUnenrolled = mdmEnrollmentStatus === "Off" || !mdmEnrollmentStatus; const showTurnOnMdmInfoBanner = !showingAppWideBanner && @@ -62,31 +77,53 @@ const HostDetailsBanners = ({ isMdmUnenrolled && config?.mdm.enabled_and_configured; - const showDiskEncryptionUserActionRequired = + const showMacDiskEncryptionUserActionRequired = !showingAppWideBanner && config?.mdm.enabled_and_configured && connectedToFleetMdm && - diskEncryptionStatus === "action_required"; + macDiskEncryptionStatus === "action_required"; - if (showTurnOnMdmInfoBanner || showDiskEncryptionUserActionRequired) { + if (showTurnOnMdmInfoBanner) { return (
- {showTurnOnMdmInfoBanner && ( - - To enforce settings, OS updates, disk encryption, and more, ask the - end user to follow the Turn on MDM instructions on - their My device page. - - )} - {showDiskEncryptionUserActionRequired && ( - - Disk encryption: Requires action from the end user. Ask the end user - to log out of their device or restart it. - - )} + + To enforce settings, OS updates, disk encryption, and more, ask the + end user to follow the Turn on MDM instructions on + their My device page. +
); } + if (showMacDiskEncryptionUserActionRequired) { + return ( +
+ + Disk encryption: Requires action from the end user. Ask the end user + to log out of their device or restart it. + +
+ ); + } + // setting applies + if ( + hostPlatform && + platformSupportsDiskEncryption(hostPlatform, hostOsVersion) && + diskEncryptionOSSetting?.status + ) { + // host either not in compliance with setting, or is but Fleet doesn't yet have a disk + // encryption key escrowed for the host (possible for Linux hosts) + if (!diskIsEncrypted || !diskEncryptionKeyAvailable) { + return ( +
+ + Disk encryption: Requires action from the end user. Ask the user to + follow Disk encryption instructions on their My device{" "} + page. + +
+ ); + } + } return null; }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx index 69e3763d4617..bb87157940a3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx @@ -8,26 +8,14 @@ import Modal from "components/Modal"; import Button from "components/buttons/Button"; import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent"; import DataError from "components/DataError"; -import { QueryablePlatform } from "interfaces/platform"; +import CustomLink from "components/CustomLink"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; +import { HostPlatform } from "interfaces/platform"; const baseClass = "disk-encryption-key-modal"; -// currently these are the only supported platforms for the disk encryption -// key modal. -export type ModalSupportedPlatform = Extract< - QueryablePlatform, - "darwin" | "windows" ->; - -// Checks to see if the platform is supported by the modal. -export const isSupportedPlatform = ( - platform: string -): platform is ModalSupportedPlatform => { - return ["darwin", "windows"].includes(platform); -}; - interface IDiskEncryptionKeyModal { - platform: ModalSupportedPlatform; + platform: HostPlatform; hostId: number; onCancel: () => void; } @@ -37,7 +25,7 @@ const DiskEncryptionKeyModal = ({ hostId, onCancel, }: IDiskEncryptionKeyModal) => { - const { data: encrpytionKey, error: encryptionKeyError } = useQuery< + const { data: encryptionKey, error: encryptionKeyError } = useQuery< IHostEncrpytionKeyResponse, unknown, string @@ -49,14 +37,10 @@ const DiskEncryptionKeyModal = ({ select: (data) => data.encryption_key.key, }); - const isMacOS = platform === "darwin"; - const descriptionText = isMacOS - ? "The disk encryption key refers to the FileVault recovery key for macOS." - : "The disk encryption key refers to the BitLocker recovery key for Windows."; - - const recoveryText = isMacOS - ? "Use this key to log in to the host if you forgot the password." - : "Use this key to unlock the encrypted drive."; + const recoveryText = + platform === "darwin" + ? "Use this key to log in to the host if you forgot the password." + : "Use this key to unlock the encrypted drive."; return ( ) : ( <> - -

{descriptionText}

-

{recoveryText}

+ +

+ {recoveryText}{" "} + +

diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx index f86226a967c6..f73601f9319e 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx @@ -29,8 +29,7 @@ const OSSettingsModal = ({ onClose, onProfileResent, }: IOSSettingsModalProps) => { - // the caller should ensure that hostMDMData is not undefined and that platform is "windows" or - // "darwin", otherwise we will allow an empty modal will be rendered. + // the caller should ensure that hostMDMData is not undefined and that platform is supported otherwise we will allow an empty modal will be rendered. // https://fleetdm.com/handbook/company/why-this-way#why-make-it-obvious-when-stuff-breaks const memoizedTableData = useMemo( diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx index d3e515908ce1..4fe684de77d6 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx @@ -4,7 +4,11 @@ import { uniqueId } from "lodash"; import Icon from "components/Icon"; import TextCell from "components/TableContainer/DataTable/TextCell"; -import { ProfileOperationType } from "interfaces/mdm"; +import { + LinuxDiskEncryptionStatus, + ProfileOperationType, + ProfilePlatform, +} from "interfaces/mdm"; import { COLORS } from "styles/var/colors"; import { @@ -14,6 +18,7 @@ import { import TooltipContent from "./components/Tooltip/TooltipContent"; import { isDiskEncryptionProfile, + LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG, PROFILE_DISPLAY_CONFIG, ProfileDisplayOption, WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG, @@ -25,22 +30,27 @@ interface IOSSettingStatusCellProps { status: OsSettingsTableStatusValue; operationType: ProfileOperationType | null; profileName: string; + hostPlatform?: ProfilePlatform; } const OSSettingStatusCell = ({ status, operationType, profileName = "", + hostPlatform, }: IOSSettingStatusCellProps) => { let displayOption: ProfileDisplayOption = null; + if (hostPlatform === "linux") { + displayOption = + LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG[status as LinuxDiskEncryptionStatus]; + } + // windows hosts do not have an operation type at the moment and their display options are // different than mac hosts. - if (!operationType && isMdmProfileStatus(status)) { + else if (!operationType && isMdmProfileStatus(status)) { displayOption = WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG[status]; - } - - if (operationType) { + } else if (operationType) { displayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status]; } diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts index 588e4e8d2954..60996eab61ee 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts @@ -135,3 +135,27 @@ export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDispla tooltip: null, }, }; + +type LinuxDiskEncryptionDisplayConfig = Omit< + OperationTypeOption, + "success" | "pending" | "acknowledged" | "verifying" +>; + +export const LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG: LinuxDiskEncryptionDisplayConfig = { + verified: { + statusText: "Verified", + iconName: "success", + tooltip: () => + "The host turned disk encryption on and sent the key to Fleet. Fleet verified.", + }, + failed: { + statusText: "Failed", + iconName: "error", + tooltip: null, + }, + action_required: { + statusText: "Action required (pending)", + iconName: "pending-outline", + tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc, + }, +}; diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx index 8f6d2650c93a..0d9b4017c87f 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx @@ -9,12 +9,17 @@ import { IHostMdmProfile, MdmDDMProfileStatus, MdmProfileStatus, + isLinuxDiskEncryptionStatus, isWindowsDiskEncryptionStatus, } from "interfaces/mdm"; +import { isDiskEncryptionSupportedLinuxPlatform } from "interfaces/platform"; import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell"; import OSSettingStatusCell from "./OSSettingStatusCell"; -import { generateWinDiskEncryptionProfile } from "../../helpers"; +import { + generateLinuxDiskEncryptionSetting, + generateWinDiskEncryptionSetting, +} from "../../helpers"; import OSSettingsErrorCell from "./OSSettingsErrorCell"; export const isMdmProfileStatus = ( @@ -69,6 +74,7 @@ const generateTableConfig = ( status={cellProps.row.original.status} operationType={cellProps.row.original.operation_type} profileName={cellProps.row.original.name} + hostPlatform={cellProps.row.original.platform} /> ); }, @@ -101,7 +107,33 @@ const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => { isWindowsDiskEncryptionStatus(os_settings.disk_encryption.status) ) { rows.push( - generateWinDiskEncryptionProfile( + generateWinDiskEncryptionSetting( + os_settings.disk_encryption.status, + os_settings.disk_encryption.detail + ) + ); + } + + if (rows.length === 0 && !profiles) { + return null; + } + + return rows; +}; + +const makeLinuxRows = ({ profiles, os_settings }: IHostMdmData) => { + const rows: IHostMdmProfileWithAddedStatus[] = []; + + if (profiles) { + rows.push(...profiles); + } + + if ( + os_settings?.disk_encryption?.status && + isLinuxDiskEncryptionStatus(os_settings.disk_encryption.status) + ) { + rows.push( + generateLinuxDiskEncryptionSetting( os_settings.disk_encryption.status, os_settings.disk_encryption.detail ) @@ -145,6 +177,10 @@ export const generateTableData = ( return makeWindowsRows(hostMDMData); case "darwin": return makeDarwinRows(hostMDMData); + case "ubuntu": + return makeLinuxRows(hostMDMData); + case "rhel": + return makeLinuxRows(hostMDMData); case "ios": return hostMDMData.profiles; case "ipados": diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss index dc330199451c..9469111a6ddb 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss @@ -1,18 +1,22 @@ .os-settings-table { - // stylings for the table cells. This was the explicit width we want // for these cells in the table. Total width of the table cell will be // 240px including the padding. - .data-table-block .data-table tbody td { - .os-settings-name-cell { - width: 135px; - max-width: none; - } - .os-settings-status-cell { - width: 200px; + .data-table-block .data-table { + &__wrapper { + width: initial; } - .os-settings-error-cell { - width: 237px; + tbody td { + .os-settings-name-cell { + width: 135px; + max-width: none; + } + .os-settings-status-cell { + width: 200px; + } + .os-settings-error-cell { + width: 237px; + } } } diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 63caddf9e0fa..1c9c8e52ad34 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -6,9 +6,17 @@ import { IHostMdmProfile, BootstrapPackageStatus, isWindowsDiskEncryptionStatus, + isLinuxDiskEncryptionStatus, } from "interfaces/mdm"; import { IOSSettings, IHostMaintenanceWindow } from "interfaces/host"; import { IAppleDeviceUpdates } from "interfaces/config"; +import { + DiskEncryptionSupportedPlatform, + isDiskEncryptionSupportedLinuxPlatform, + isOsSettingsDisplayPlatform, + platformSupportsDiskEncryption, +} from "interfaces/platform"; + import getHostStatusTooltipText from "pages/hosts/helpers"; import TooltipWrapper from "components/TooltipWrapper"; @@ -37,7 +45,8 @@ import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPack import { HostMdmDeviceStatusUIState, - generateWinDiskEncryptionProfile, + generateLinuxDiskEncryptionSetting, + generateWinDiskEncryptionSetting, } from "../../helpers"; import { DEVICE_STATUS_TAGS, REFETCH_TOOLTIP_MESSAGES } from "./helpers"; @@ -118,8 +127,7 @@ interface IHostSummaryProps { isPremiumTier?: boolean; toggleOSSettingsModal?: () => void; toggleBootstrapPackageModal?: () => void; - hostMdmProfiles?: IHostMdmProfile[]; - isConnectedToFleetMdm?: boolean; + hostSettings?: IHostMdmProfile[]; showRefetchSpinner: boolean; onRefetchHost: ( evt: React.MouseEvent @@ -131,7 +139,7 @@ interface IHostSummaryProps { hostMdmDeviceStatus?: HostMdmDeviceStatusUIState; } -const MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES = { +const DISK_ENCRYPTION_MESSAGES = { darwin: { enabled: ( <> @@ -155,20 +163,28 @@ const MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES = { ), disabled: "The disk is unencrypted.", }, + linux: { + enabled: "The disk is encrypted.", + unknown: "The disk may be encrypted.", + }, }; const getHostDiskEncryptionTooltipMessage = ( - platform: "darwin" | "windows" | "chrome", // TODO: improve this type + platform: DiskEncryptionSupportedPlatform, // TODO: improve this type diskEncryptionEnabled = false ) => { if (platform === "chrome") { return "Fleet does not check for disk encryption on Chromebooks, as they are encrypted by default."; } - if (!["windows", "darwin"].includes(platform)) { - return "Disk encryption is enabled."; + if (platform === "rhel" || platform === "ubuntu") { + return DISK_ENCRYPTION_MESSAGES.linux[ + diskEncryptionEnabled ? "enabled" : "unknown" + ]; } - return MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES[platform][ + + // mac or windows + return DISK_ENCRYPTION_MESSAGES[platform][ diskEncryptionEnabled ? "enabled" : "disabled" ]; }; @@ -179,8 +195,7 @@ const HostSummary = ({ isPremiumTier, toggleOSSettingsModal, toggleBootstrapPackageModal, - hostMdmProfiles, - isConnectedToFleetMdm, + hostSettings, showRefetchSpinner, onRefetchHost, renderActionDropdown, @@ -192,6 +207,7 @@ const HostSummary = ({ const { status, platform, + os_version, disk_encryption_enabled: diskEncryptionEnabled, } = summaryData; @@ -281,8 +297,7 @@ const HostSummary = ({ ); }; const renderDiskEncryptionSummary = () => { - // TODO: improve this typing, platforms! - if (!["darwin", "windows", "chrome"].includes(platform)) { + if (!platformSupportsDiskEncryption(platform, os_version)) { return <>; } const tooltipMessage = getHostDiskEncryptionTooltipMessage( @@ -301,6 +316,11 @@ const HostSummary = ({ case diskEncryptionEnabled === false: statusText = "Off"; break; + case (diskEncryptionEnabled === null || + diskEncryptionEnabled === undefined) && + platformSupportsDiskEncryption(platform, os_version): + statusText = "Unknown"; + break; default: // something unexpected happened on the way to this component, display whatever we got or // "Unknown" to draw attention to the issue. @@ -441,21 +461,35 @@ const HostSummary = ({ }; const renderSummary = () => { - // for windows hosts we have to manually add a profile for disk encryption + // for windows and linux hosts we have to manually add a profile for disk encryption // as this is not currently included in the `profiles` value from the API - // response for windows hosts. + // response for windows and linux hosts. if ( platform === "windows" && osSettings?.disk_encryption?.status && isWindowsDiskEncryptionStatus(osSettings.disk_encryption.status) ) { - const winDiskEncryptionProfile: IHostMdmProfile = generateWinDiskEncryptionProfile( + const winDiskEncryptionSetting: IHostMdmProfile = generateWinDiskEncryptionSetting( + osSettings.disk_encryption.status, + osSettings.disk_encryption.detail + ); + hostSettings = hostSettings + ? [...hostSettings, winDiskEncryptionSetting] + : [winDiskEncryptionSetting]; + } + + if ( + isDiskEncryptionSupportedLinuxPlatform(platform, os_version) && + osSettings?.disk_encryption?.status && + isLinuxDiskEncryptionStatus(osSettings.disk_encryption.status) + ) { + const linuxDiskEncryptionSetting: IHostMdmProfile = generateLinuxDiskEncryptionSetting( osSettings.disk_encryption.status, osSettings.disk_encryption.detail ); - hostMdmProfiles = hostMdmProfiles - ? [...hostMdmProfiles, winDiskEncryptionProfile] - : [winDiskEncryptionProfile]; + hostSettings = hostSettings + ? [...hostSettings, linuxDiskEncryptionSetting] + : [linuxDiskEncryptionSetting]; } return ( @@ -484,19 +518,15 @@ const HostSummary = ({ renderIssues()} {isPremiumTier && renderHostTeam()} {/* Rendering of OS Settings data */} - {(platform === "darwin" || - platform === "windows" || - platform === "ios" || - platform === "ipados") && + {isOsSettingsDisplayPlatform(platform, os_version) && isPremiumTier && - isConnectedToFleetMdm && // show if 1 - host is enrolled in Fleet MDM, and - hostMdmProfiles && - hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced + hostSettings && + hostSettings.length > 0 && ( } diff --git a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx index 60cc9c069b6d..3a145e4f0e89 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx @@ -47,7 +47,7 @@ const countHostProfilesByStatus = ( (acc, { status }) => { if (status === "failed") { acc.failed += 1; - } else if (status === "pending") { + } else if (status === "pending" || status === "action_required") { acc.pending += 1; } else if (status === "verifying") { acc.verifying += 1; diff --git a/frontend/pages/hosts/details/helpers.ts b/frontend/pages/hosts/details/helpers.ts index 52a0efd6ddaf..5c78a68bc9ad 100644 --- a/frontend/pages/hosts/details/helpers.ts +++ b/frontend/pages/hosts/details/helpers.ts @@ -2,12 +2,13 @@ import { HostMdmDeviceStatus, HostMdmPendingAction } from "interfaces/host"; import { IHostMdmProfile, - IWindowsDiskEncryptionStatus, + WindowsDiskEncryptionStatus, MdmProfileStatus, + LinuxDiskEncryptionStatus, } from "interfaces/mdm"; -const convertWinDiskEncryptionStatusToProfileStatus = ( - diskEncryptionStatus: IWindowsDiskEncryptionStatus +const convertWinDiskEncryptionStatusToSettingStatus = ( + diskEncryptionStatus: WindowsDiskEncryptionStatus ): MdmProfileStatus => { return diskEncryptionStatus === "enforcing" ? "pending" @@ -15,20 +16,40 @@ const convertWinDiskEncryptionStatusToProfileStatus = ( }; /** - * Manually generates a profile for the windows disk encryption status. We need + * Manually generates a setting for the windows disk encryption status. We need * this as we don't have a windows disk encryption profile in the `profiles` * attribute coming back from the GET /hosts/:id API response. */ // eslint-disable-next-line import/prefer-default-export -export const generateWinDiskEncryptionProfile = ( - diskEncryptionStatus: IWindowsDiskEncryptionStatus, +export const generateWinDiskEncryptionSetting = ( + diskEncryptionStatus: WindowsDiskEncryptionStatus, detail: string ): IHostMdmProfile => { return { profile_uuid: "0", // This s the only type of profile that can have this value platform: "windows", name: "Disk Encryption", - status: convertWinDiskEncryptionStatusToProfileStatus(diskEncryptionStatus), + status: convertWinDiskEncryptionStatusToSettingStatus(diskEncryptionStatus), + detail, + operation_type: null, + }; +}; + +/** + * Manually generates a setting for the linux disk encryption status. We need + * this as we don't have a linux disk encryption setting in the `profiles` + * attribute coming back from the GET /hosts/:id API response. + */ +// eslint-disable-next-line import/prefer-default-export +export const generateLinuxDiskEncryptionSetting = ( + diskEncryptionStatus: LinuxDiskEncryptionStatus, + detail: string +): IHostMdmProfile => { + return { + profile_uuid: "0", // This s the only type of profile that can have this value + platform: "linux", + name: "Disk Encryption", + status: diskEncryptionStatus, detail, operation_type: null, }; diff --git a/frontend/services/entities/disk_encryption.ts b/frontend/services/entities/disk_encryption.ts new file mode 100644 index 000000000000..50a0eb63ef7d --- /dev/null +++ b/frontend/services/entities/disk_encryption.ts @@ -0,0 +1,60 @@ +import sendRequest from "services"; + +import endpoints from "utilities/endpoints"; +import { buildQueryStringFromParams } from "utilities/url"; + +// TODO - move disk encryption types like this to dedicated file +import { DiskEncryptionStatus } from "interfaces/mdm"; +import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; + +export interface IDiskEncryptionStatusAggregate { + macos: number; + windows: number; + linux: number; +} + +export type IDiskEncryptionSummaryResponse = Record< + DiskEncryptionStatus, + IDiskEncryptionStatusAggregate +>; + +const diskEncryptionService = { + getDiskEncryptionSummary: (teamId?: number) => { + let { MDM_DISK_ENCRYPTION_SUMMARY: path } = endpoints; + + if (teamId) { + path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`; + } + return sendRequest("GET", path); + }, + updateDiskEncryption: (enableDiskEncryption: boolean, teamId?: number) => { + // TODO - use same endpoint for both once issue with new endpoint for no team is resolved + const { + UPDATE_DISK_ENCRYPTION: teamsEndpoint, + CONFIG: noTeamsEndpoint, + } = endpoints; + if (teamId === 0) { + return sendRequest("PATCH", noTeamsEndpoint, { + mdm: { + enable_disk_encryption: enableDiskEncryption, + }, + }); + } + return sendRequest("POST", teamsEndpoint, { + enable_disk_encryption: enableDiskEncryption, + // TODO - it would be good to be able to use an API_CONTEXT_NO_TEAM_ID here, but that is + // currently set to 0, which should actually be undefined since the server expects teamId == + // nil for no teams, not 0. + team_id: teamId === APP_CONTEXT_NO_TEAM_ID ? undefined : teamId, + }); + }, + triggerLinuxDiskEncryptionKeyEscrow: (token: string) => { + const { DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW } = endpoints; + return sendRequest( + "POST", + DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW(token) + ); + }, +}; + +export default diskEncryptionService; diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index 50ed3bd428a8..ec7499390b40 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -1,5 +1,4 @@ import { - DiskEncryptionStatus, IHostMdmProfile, IMdmCommandResult, IMdmProfile, @@ -21,16 +20,6 @@ export interface IEulaMetadataResponse { export type ProfileStatusSummaryResponse = Record; -export interface IDiskEncryptionStatusAggregate { - macos: number; - windows: number; -} - -export type IDiskEncryptionSummaryResponse = Record< - DiskEncryptionStatus, - IDiskEncryptionStatusAggregate ->; - export interface IGetProfilesApiParams { page?: number; per_page?: number; @@ -188,37 +177,6 @@ const mdmService = { return sendRequest("GET", path); }, - getDiskEncryptionSummary: (teamId?: number) => { - let { MDM_DISK_ENCRYPTION_SUMMARY: path } = endpoints; - - if (teamId) { - path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`; - } - return sendRequest("GET", path); - }, - - // TODO: API INTEGRATION: change when API is implemented that works for windows - // disk encryption too. - updateAppleMdmSettings: (enableDiskEncryption: boolean, teamId?: number) => { - const { - MDM_UPDATE_APPLE_SETTINGS: teamsEndpoint, - CONFIG: noTeamsEndpoint, - } = endpoints; - if (teamId === 0) { - return sendRequest("PATCH", noTeamsEndpoint, { - mdm: { - // TODO: API INTEGRATION: remove macos_settings when API change is merged in. - macos_settings: { enable_disk_encryption: enableDiskEncryption }, - // enable_disk_encryption: enableDiskEncryption, - }, - }); - } - return sendRequest("PATCH", teamsEndpoint, { - enable_disk_encryption: enableDiskEncryption, - team_id: teamId, - }); - }, - initiateMDMAppleSSO: () => { const { MDM_APPLE_SSO } = endpoints; return sendRequest("POST", MDM_APPLE_SSO, {}); diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index bb6b3dea65b9..196345bf2bbe 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -34,6 +34,9 @@ export default { DEVICE_USER_MDM_ENROLLMENT_PROFILE: (token: string): string => { return `/${API_VERSION}/fleet/device/${token}/mdm/apple/manual_enrollment_profile`; }, + DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW: (token: string): string => { + return `/${API_VERSION}/fleet/device/${token}/mdm/linux/trigger_escrow`; + }, // Host endpoints HOST_SUMMARY: `/${API_VERSION}/fleet/host_summary`, @@ -138,6 +141,9 @@ export default { ME: `/${API_VERSION}/fleet/me`, + // Disk encryption endpoints + UPDATE_DISK_ENCRYPTION: `/${API_VERSION}/fleet/disk_encryption`, + // Setup experiece endpoints MDM_SETUP_EXPERIENCE: `/${API_VERSION}/fleet/setup_experience`, MDM_SETUP_EXPERIENCE_SOFTWARE: `/${API_VERSION}/fleet/setup_experience/software`, diff --git a/server/fleet/service.go b/server/fleet/service.go index 9eb03ec2f73e..97a74aaa014c 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1055,8 +1055,8 @@ type Service interface { /////////////////////////////////////////////////////////////////////////////// // Common MDM - // GetMDMDiskEncryptionSummary returns the current disk encryption status of all macOS and - // Windows hosts in the specified team (or, if no team is specified, each host that is not + // GetMDMDiskEncryptionSummary returns the current disk encryption status of all macOS, Windows, and + // Linux hosts in the specified team (or, if no team is specified, each host that is not // assigned to any team). GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*MDMDiskEncryptionSummary, error) diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 561850901271..ad8778b57035 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -416,6 +416,9 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle // 1. To get the JSON value from the database // 2. To update fields with the incoming values if newAppConfig.MDM.EnableDiskEncryption.Valid { + if svc.config.Server.PrivateKey == "" { + return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } appConfig.MDM.EnableDiskEncryption = newAppConfig.MDM.EnableDiskEncryption } else if appConfig.MDM.EnableDiskEncryption.Set && !appConfig.MDM.EnableDiskEncryption.Valid { appConfig.MDM.EnableDiskEncryption = oldAppConfig.MDM.EnableDiskEncryption @@ -1130,15 +1133,6 @@ func (svc *Service) validateMDM( return nil } } - - // if either macOS or Windows MDM is enabled, this setting can be set. - if !mdm.AtLeastOnePlatformEnabledAndConfigured() { - if mdm.EnableDiskEncryption.Valid && mdm.EnableDiskEncryption.Value && mdm.EnableDiskEncryption.Value != oldMdm.EnableDiskEncryption.Value { - invalid.Append("mdm.enable_disk_encryption", - `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`) - } - } - return nil } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 1e601997c45e..734ef43f1c18 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1208,6 +1208,59 @@ func TestMDMAppleConfig(t *testing.T) { } } +func TestDiskEncryptionSetting(t *testing.T) { + ds := new(mock.Store) + + admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)} + t.Run("enableDiskEncryptionWithNoPrivateKey", func(t *testing.T) { + testConfig = config.TestConfig() + testConfig.Server.PrivateKey = "" + svc, ctx := newTestServiceWithConfig(t, ds, testConfig, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) + ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin}) + + dsAppConfig := &fleet.AppConfig{ + OrgInfo: fleet.OrgInfo{OrgName: "Test"}, + ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}, + MDM: fleet.MDM{}, + } + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return dsAppConfig, nil + } + + ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error { + *dsAppConfig = *conf + return nil + } + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + return nil, sql.ErrNoRows + } + ds.NewMDMAppleEnrollmentProfileFunc = func(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) { + return &fleet.MDMAppleEnrollmentProfile{}, nil + } + ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) { + raw := json.RawMessage("{}") + return &fleet.MDMAppleEnrollmentProfile{DEPProfile: &raw}, nil + } + ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) { + return job, nil + } + + ac, err := svc.AppConfigObfuscated(ctx) + require.NoError(t, err) + require.Equal(t, dsAppConfig.MDM, ac.MDM) + + raw, err := json.Marshal(fleet.MDM{ + EnableDiskEncryption: optjson.SetBool(true), + }) + require.NoError(t, err) + raw = []byte(`{"mdm":` + string(raw) + `}`) + _, err = svc.ModifyAppConfig(ctx, raw, fleet.ApplySpecOptions{}) + require.Error(t, err) + require.ErrorContains(t, err, "Missing required private key") + }) +} + func TestModifyAppConfigSMTPSSOAgentOptions(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 60ae723f9a7a..fc3bc4af70f3 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -1939,7 +1939,7 @@ func TestUpdateMDMAppleSettings(t *testing.T) { &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, false, nil, - ErrMissingLicense.Error(), + fleet.ErrMissingLicense.Error(), }, { "global admin premium", @@ -1960,7 +1960,7 @@ func TestUpdateMDMAppleSettings(t *testing.T) { &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, false, nil, - ErrMissingLicense.Error(), + fleet.ErrMissingLicense.Error(), }, { "global maintainer premium", @@ -2037,7 +2037,7 @@ func TestUpdateMDMAppleSettings(t *testing.T) { &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, false, ptr.Uint(1), - ErrMissingLicense.Error(), + fleet.ErrMissingLicense.Error(), }, } diff --git a/server/service/handler.go b/server/service/handler.go index 147acacf4402..48aab3e44d48 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -696,8 +696,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Deprecated: GET /mdm/disk_encryption/summary is now deprecated, replaced by the // GET /disk_encryption endpoint. - mdmAnyMW.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{}) - mdmAnyMW.GET("/api/_version_/fleet/disk_encryption", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{}) + ue.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{}) + ue.GET("/api/_version_/fleet/disk_encryption", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{}) // Deprecated: GET /mdm/hosts/:id/encryption_key is now deprecated, replaced by // GET /hosts/:id/encryption_key. @@ -706,8 +706,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Deprecated: GET /mdm/profiles/summary is now deprecated, replaced by the // GET /configuration_profiles/summary endpoint. - mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{}) - mdmAnyMW.GET("/api/_version_/fleet/configuration_profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{}) + ue.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{}) + ue.GET("/api/_version_/fleet/configuration_profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{}) // Deprecated: GET /mdm/profiles/:profile_uuid is now deprecated, replaced by // GET /configuration_profiles/:profile_uuid. @@ -734,7 +734,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Deprecated: PATCH /mdm/apple/settings is deprecated, replaced by POST /disk_encryption. // It was only used to set disk encryption. mdmAnyMW.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{}) - mdmAnyMW.POST("/api/_version_/fleet/disk_encryption", updateMDMDiskEncryptionEndpoint, updateMDMDiskEncryptionRequest{}) + ue.POST("/api/_version_/fleet/disk_encryption", updateDiskEncryptionEndpoint, updateDiskEncryptionRequest{}) // the following set of mdm endpoints must always be accessible (even // if MDM is not configured) as it bootstraps the setup of MDM diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 45c56a3d7642..999b6bfb34d6 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6223,10 +6223,8 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() { errMsg := extractServerErrorText(res.Body) require.Contains(t, errMsg, "Fleet MDM is not configured") - // update MDM disk encryption, the endpoint returns an error if MDM is not enabled - res = s.Do("POST", "/api/latest/fleet/disk_encryption", fleet.MDMAppleSettingsPayload{}, fleet.ErrMDMNotConfigured.StatusCode()) - errMsg = extractServerErrorText(res.Body) - require.Contains(t, errMsg, fleet.ErrMDMNotConfigured.Error()) + // update MDM disk encryption + _ = s.Do("POST", "/api/latest/fleet/disk_encryption", fleet.MDMAppleSettingsPayload{}, http.StatusPaymentRequired) // device migrate mdm endpoint returns an error if not premium createHostAndDeviceToken(t, s.ds, "some-token") diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index ddd1f5822b8e..f9b7b352576b 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -1656,45 +1656,6 @@ func (s *integrationMDMTestSuite) TestDiskEncryptionSharedSetting() { require.NoError(s.T(), err) }) - checkConfigSetErrors := func() { - // try to set app config - res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "enable_disk_encryption": true } - }`), http.StatusUnprocessableEntity) - errMsg := extractServerErrorText(res.Body) - require.Contains(t, errMsg, "Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.") - - // try to create a new team using specs - teamSpecs := map[string]any{ - "specs": []any{ - map[string]any{ - "name": teamName + uuid.NewString(), - "mdm": map[string]any{ - "enable_disk_encryption": true, - }, - }, - }, - } - res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity) - errMsg = extractServerErrorText(res.Body) - require.Contains(t, errMsg, "Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.") - - // try to edit the existing team using specs - teamSpecs = map[string]any{ - "specs": []any{ - map[string]any{ - "name": teamName, - "mdm": map[string]any{ - "enable_disk_encryption": true, - }, - }, - }, - } - res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity) - errMsg = extractServerErrorText(res.Body) - require.Contains(t, errMsg, "Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.") - } - checkConfigSetSucceeds := func() { res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "enable_disk_encryption": true } @@ -1749,10 +1710,9 @@ func (s *integrationMDMTestSuite) TestDiskEncryptionSharedSetting() { s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) } - // disable both windows and mac mdm - // we should get an error + // MDM config succeeds because we have a private key baked into default suite config setMDMEnabled(false, false) - checkConfigSetErrors() + checkConfigSetSucceeds() // enable windows mdm, no errors setMDMEnabled(false, true) @@ -2218,7 +2178,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { // use the MDM disk encryption endpoint to set it to true s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent) + updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent) enabledDiskActID = s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), `{"team_id": null, "team_name": null}`, 0) @@ -2263,13 +2223,13 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { // flip and verify the value s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{EnableDiskEncryption: false}, http.StatusNoContent) + updateDiskEncryptionRequest{EnableDiskEncryption: false}, http.StatusNoContent) acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) assert.False(t, acResp.MDM.EnableDiskEncryption.Value) s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent) + updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent) acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) assert.True(t, acResp.MDM.EnableDiskEncryption.Value) @@ -2573,7 +2533,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { // use the MDM settings endpoint to set it to true s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: true}, http.StatusNoContent) + updateDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: true}, http.StatusNoContent) lastDiskActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0) @@ -2599,7 +2559,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { // use the MDM settings endpoint with an unknown team id s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{TeamID: ptr.Uint(9999), EnableDiskEncryption: true}, http.StatusNotFound) + updateDiskEncryptionRequest{TeamID: ptr.Uint(9999), EnableDiskEncryption: true}, http.StatusNotFound) // mdm/apple/settings works for windows as well as it's being used by // clients (UI) this way @@ -2620,13 +2580,13 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { // flip and verify the value s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: false}, http.StatusNoContent) + updateDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: false}, http.StatusNoContent) teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) require.False(t, teamResp.Team.Config.MDM.EnableDiskEncryption) s.Do("POST", "/api/latest/fleet/disk_encryption", - updateMDMDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: true}, http.StatusNoContent) + updateDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: true}, http.StatusNoContent) teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption) @@ -7572,7 +7532,7 @@ func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() { // TODO: Some global MDM config settings don't have MDMEnabledAndConfigured or // WindowsMDMEnabledAndConfigured validations currently. Either add validations - // and test them or test abscence of validation. + // and test them or test absence of validation. t.Run("apply app config spec", func(t *testing.T) { t.Run("disk encryption", func(t *testing.T) { t.Cleanup(func() { @@ -7605,14 +7565,14 @@ func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() { require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // no change require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) - // disabling disk encryption doesn't cause validation error because Windows is still enabled + // disabling disk encryption doesn't cause validation error ac.MDM.EnableDiskEncryption = optjson.SetBool(false) s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) acResp = checkAppConfig(t, false, true) // only windows mdm enabled require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) - // enabling disk encryption doesn't cause validation error because Windows is still enabled + // enabling disk encryption doesn't cause validation error ac.MDM.EnableDiskEncryption = optjson.SetBool(true) s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) @@ -7625,25 +7585,26 @@ func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() { acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabling mdm doesn't change disk encryption + // disabling disk encryption doesn't cause validation error + ac.MDM.EnableDiskEncryption = optjson.SetBool(false) + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // no MDM enabled + require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled + require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName) + + // enabling disk encryption doesn't cause validation error + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + acResp = checkAppConfig(t, false, false) // no MDM enabled + require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // enabled + // changing unrelated config doesn't cause validation error ac.OrgInfo.OrgName = "f1338" s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // no change require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName) - - // changing MDM config doesn't cause validation error when switching to default values - ac.MDM.EnableDiskEncryption = optjson.SetBool(false) - // TODO: Should it be ok to disable disk encryption when MDM is disabled? - s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp) - acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled - require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // changed to disabled - - // changing MDM config does cause validation error when switching to non-default vailes - ac.MDM.EnableDiskEncryption = optjson.SetBool(true) - s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp) - acResp = checkAppConfig(t, false, false) // both mac and windows mdm disabled - require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // still disabled }) t.Run("macos setup", func(t *testing.T) { @@ -8107,17 +8068,18 @@ func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() { }) checkTeam := func(t *testing.T, team *fleet.Team, checkMDM *fleet.TeamSpecMDM) teamResponse { - var wantDiskEncryption bool + // TODO - remove check of disk encryption from this function entirely? + // var wantDiskEncryption bool var wantMacOSSetup fleet.MacOSSetup if checkMDM != nil { wantMacOSSetup = checkMDM.MacOSSetup - wantDiskEncryption = checkMDM.EnableDiskEncryption.Value + // wantDiskEncryption = checkMDM.EnableDiskEncryption.Value } var resp teamResponse s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &resp) require.Equal(t, team.Name, resp.Team.Name) - require.Equal(t, wantDiskEncryption, resp.Team.Config.MDM.EnableDiskEncryption) + // require.Equal(t, wantDiskEncryption, resp.Team.Config.MDM.EnableDiskEncryption) require.Equal(t, wantMacOSSetup.BootstrapPackage.Value, resp.Team.Config.MDM.MacOSSetup.BootstrapPackage.Value) require.Equal(t, wantMacOSSetup.MacOSSetupAssistant.Value, resp.Team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value) require.Equal(t, wantMacOSSetup.EnableEndUserAuthentication, resp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) @@ -8195,9 +8157,10 @@ func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() { &fleet.TeamSpecMDM{ EnableDiskEncryption: optjson.SetBool(true), }, - // disk encryption requires mdm enabled and configured - http.StatusUnprocessableEntity, + // disk encryption does not require mdm enabled and configured + http.StatusOK, }, + // Ian - this test still passes, that is, returns 4xx – perhaps related to one of the endpoints we still need to update { "enable end user auth", &fleet.TeamSpecMDM{ diff --git a/server/service/mdm.go b/server/service/mdm.go index 92524ba1f118..12876b95ddd7 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -2165,7 +2165,7 @@ func (svc *Service) ListMDMConfigProfiles(ctx context.Context, teamID *uint, opt // Update MDM Disk encryption //////////////////////////////////////////////////////////////////////////////// -type updateMDMDiskEncryptionRequest struct { +type updateDiskEncryptionRequest struct { TeamID *uint `json:"team_id"` EnableDiskEncryption bool `json:"enable_disk_encryption"` } @@ -2178,8 +2178,8 @@ func (r updateMDMDiskEncryptionResponse) error() error { return r.Err } func (r updateMDMDiskEncryptionResponse) Status() int { return http.StatusNoContent } -func updateMDMDiskEncryptionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*updateMDMDiskEncryptionRequest) +func updateDiskEncryptionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*updateDiskEncryptionRequest) if err := svc.UpdateMDMDiskEncryption(ctx, req.TeamID, &req.EnableDiskEncryption); err != nil { return updateMDMDiskEncryptionResponse{Err: err}, nil } @@ -2194,7 +2194,7 @@ func (svc *Service) UpdateMDMDiskEncryption(ctx context.Context, teamID *uint, e lic, _ := license.FromContext(ctx) if lic == nil || !lic.IsPremium() { svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden" - return ErrMissingLicense + return fleet.ErrMissingLicense } // for historical reasons (the deprecated PATCH /mdm/apple/settings diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 99a0851bdc1d..4fc4ef78a0fb 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -684,18 +684,13 @@ func mdmConfigurationRequiredEndpoints() []struct { {"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}, - {"GET", "/api/latest/fleet/mdm/profiles/summary", false, false}, - {"GET", "/api/latest/fleet/configuration_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/encryption_key", false, false}, - {"GET", "/api/latest/fleet/hosts/1/encryption_key", 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}, - {"POST", "/api/latest/fleet/disk_encryption", 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}, @@ -725,8 +720,6 @@ func mdmConfigurationRequiredEndpoints() []struct { {"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/disk_encryption/summary", false, true}, - {"GET", "/api/latest/fleet/disk_encryption", false, true}, {"GET", "/api/latest/fleet/mdm/profiles/1", false, false}, {"GET", "/api/latest/fleet/configuration_profiles/1", false, false}, {"DELETE", "/api/latest/fleet/mdm/profiles/1", false, false}, From 896e4f05a62f89ec6ee604a133220022c7042470 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:14:07 -0800 Subject: [PATCH 16/36] to RC: remove MDM middleware from 2 endpoints (#23997) (#24003) #### This PR already merged to `main`, see https://github.com/fleetdm/fleet/pull/23997. This is against the release branch so it can be included in 4.60.0. Co-authored-by: Jacob Shandling --- server/service/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/service/handler.go b/server/service/handler.go index 48aab3e44d48..d433a7b1124b 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -701,8 +701,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Deprecated: GET /mdm/hosts/:id/encryption_key is now deprecated, replaced by // GET /hosts/:id/encryption_key. - mdmAnyMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) - mdmAnyMW.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) + ue.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) + ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) // Deprecated: GET /mdm/profiles/summary is now deprecated, replaced by the // GET /configuration_profiles/summary endpoint. From 1278371b487d10a7420df5cfaceaa8f4e66902ec Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Thu, 21 Nov 2024 11:12:17 -0600 Subject: [PATCH 17/36] Cherry-Pick: Populate disk encryption status when pulling a host by device auth token (#24017) Cherry-pick of #24014, related to #23583 --- server/datastore/mysql/hosts.go | 1 + server/datastore/mysql/hosts_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index c5d74735f29b..1c74569817b7 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -2440,6 +2440,7 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available, COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available, COALESCE(hd.gigs_total_disk_space, 0) as gigs_total_disk_space, + hd.encrypted as disk_encryption_enabled, IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet FROM host_device_auth hda diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 6fe89b476839..de3fe566e730 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -6194,6 +6194,17 @@ func testHostsLoadHostByDeviceAuthToken(t *testing.T, ds *Datastore) { require.Equal(t, hSimple.ID, loadSimple.ID) require.True(t, loadSimple.IsOsqueryEnrolled()) + // make sure disk encryption state is reflected + require.Nil(t, loadSimple.DiskEncryptionEnabled) + require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hSimple.ID, false)) + loadSimple, err = ds.LoadHostByDeviceAuthToken(ctx, "simple", time.Second*3) + require.NoError(t, err) + require.False(t, *loadSimple.DiskEncryptionEnabled) + require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hSimple.ID, true)) + loadSimple, err = ds.LoadHostByDeviceAuthToken(ctx, "simple", time.Second*3) + require.NoError(t, err) + require.True(t, *loadSimple.DiskEncryptionEnabled) + // create a host that will be pending enrollment in Fleet MDM hFleet := createHostWithDeviceToken("fleet") err = ds.SetOrUpdateMDMData(ctx, hFleet.ID, false, false, "https://fleetdm.com", true, fleet.WellKnownMDMFleet, "") From 3003f04a5f8b3316f4dc4652f4eb28de8d52c2e8 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Thu, 21 Nov 2024 12:25:35 -0500 Subject: [PATCH 18/36] Linux encryption cherry picks (#24016) Contains Agent and backend PRs #23619 #23806 #23771 --- ee/server/service/mdm.go | 8 + go.mod | 5 +- go.sum | 14 +- orbit/changes/22047-linux-key-escrow | 1 + orbit/cmd/orbit/orbit.go | 4 + orbit/pkg/dialog/dialog.go | 66 +++++ orbit/pkg/execuser/execuser.go | 30 +- orbit/pkg/execuser/execuser_darwin.go | 10 + orbit/pkg/execuser/execuser_linux.go | 106 +++++-- orbit/pkg/execuser/execuser_windows.go | 9 + orbit/pkg/luks/luks.go | 37 +++ orbit/pkg/luks/luks_linux.go | 294 +++++++++++++++++++ orbit/pkg/luks/luks_stub.go | 13 + orbit/pkg/lvm/lvm.go | 104 +++++++ orbit/pkg/lvm/lvm_test.go | 344 +++++++++++++++++++++++ orbit/pkg/zenity/zenity.go | 140 +++++++++ orbit/pkg/zenity/zenity_test.go | 268 ++++++++++++++++++ server/datastore/mysql/hosts.go | 2 +- server/datastore/mysql/linux_mdm.go | 69 +++++ server/datastore/mysql/linux_mdm_test.go | 146 ++++++++++ server/fleet/datastore.go | 8 + server/fleet/hosts.go | 1 + server/fleet/linux_mdm.go | 7 + server/fleet/mdm.go | 1 + server/fleet/service.go | 7 + server/mock/datastore_mock.go | 12 + server/service/hosts.go | 14 + server/service/hosts_test.go | 13 +- server/service/linux_mdm.go | 44 +++ server/service/linux_mdm_test.go | 118 ++++++++ server/service/mdm_test.go | 13 + server/service/orbit_client.go | 16 ++ server/test/new_objects.go | 6 + tools/dialog/main.go | 55 ++++ tools/luks/luks/main.go | 72 +++++ tools/luks/lvm/main.go | 15 + 36 files changed, 2045 insertions(+), 27 deletions(-) create mode 100644 orbit/changes/22047-linux-key-escrow create mode 100644 orbit/pkg/dialog/dialog.go create mode 100644 orbit/pkg/luks/luks.go create mode 100644 orbit/pkg/luks/luks_linux.go create mode 100644 orbit/pkg/luks/luks_stub.go create mode 100644 orbit/pkg/lvm/lvm.go create mode 100644 orbit/pkg/lvm/lvm_test.go create mode 100644 orbit/pkg/zenity/zenity.go create mode 100644 orbit/pkg/zenity/zenity_test.go create mode 100644 server/datastore/mysql/linux_mdm.go create mode 100644 server/datastore/mysql/linux_mdm_test.go create mode 100644 server/fleet/linux_mdm.go create mode 100644 server/service/linux_mdm.go create mode 100644 server/service/linux_mdm_test.go create mode 100644 tools/dialog/main.go create mode 100644 tools/luks/luks/main.go create mode 100644 tools/luks/lvm/main.go diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 354ae1e32250..674329d9e493 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -1013,10 +1013,16 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin windows = *w } + linux, err := svc.ds.GetLinuxDiskEncryptionSummary(ctx, teamID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting linux disk encryption summary") + } + return &fleet.MDMDiskEncryptionSummary{ Verified: fleet.MDMPlatformsCounts{ MacOS: macOS.Verified, Windows: windows.Verified, + Linux: linux.Verified, }, Verifying: fleet.MDMPlatformsCounts{ MacOS: macOS.Verifying, @@ -1025,6 +1031,7 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin ActionRequired: fleet.MDMPlatformsCounts{ MacOS: macOS.ActionRequired, Windows: windows.ActionRequired, + Linux: linux.ActionRequired, }, Enforcing: fleet.MDMPlatformsCounts{ MacOS: macOS.Enforcing, @@ -1033,6 +1040,7 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin Failed: fleet.MDMPlatformsCounts{ MacOS: macOS.Failed, Windows: windows.Failed, + Linux: linux.Failed, }, RemovingEnforcement: fleet.MDMPlatformsCounts{ MacOS: macOS.RemovingEnforcement, diff --git a/go.mod b/go.mod index 778e211419e0..aa464c81f768 100644 --- a/go.mod +++ b/go.mod @@ -67,6 +67,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/josephspurrier/goversioninfo v1.4.0 github.com/kevinburke/go-bindata v3.24.0+incompatible + github.com/klauspost/compress v1.17.9 github.com/kolide/launcher v1.0.12 github.com/lib/pq v1.10.9 github.com/macadmins/osquery-extension v1.2.1 @@ -97,6 +98,7 @@ require ( github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 github.com/sethvargo/go-password v0.3.0 github.com/shirou/gopsutil/v3 v3.24.3 + github.com/siderolabs/go-blockdevice/v2 v2.0.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallstep/pkcs7 v0.0.0-20240723090913-5e2c6a136dfa github.com/smallstep/scep v0.0.0-20240214080410-892e41795b99 @@ -181,6 +183,7 @@ require ( github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect github.com/antchfx/xpath v1.2.2 // indirect github.com/apache/thrift v0.18.1 // indirect + github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/atc0005/go-teams-notify/v2 v2.6.0 // indirect github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect @@ -266,7 +269,6 @@ require ( github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.8 // indirect github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -296,6 +298,7 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.5.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/siderolabs/go-cmd v0.1.1 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/slack-go/slack v0.9.4 // indirect github.com/spf13/afero v1.6.0 // indirect diff --git a/go.sum b/go.sum index 515f77b00eb9..1b90fec76b5b 100644 --- a/go.sum +++ b/go.sum @@ -233,6 +233,8 @@ github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -460,6 +462,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/freddierice/go-losetup/v2 v2.0.1 h1:wPDx/Elu9nDV8y/CvIbEDz5Xi5Zo80y4h7MKbi3XaAI= +github.com/freddierice/go-losetup/v2 v2.0.1/go.mod h1:TEyBrvlOelsPEhfWD5rutNXDmUszBXuFnwT1kIQF4J8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= @@ -817,8 +821,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab h1:KVR7cs+oPyy85i+8t1ZaNSy1bymCy5FuWyt51pdrXu4= github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= github.com/kolide/launcher v1.0.12 h1:f2uT1kKYGIbj/WVsHDc10f7MIiwu8MpmgwaGaT7D09k= @@ -1059,6 +1063,12 @@ github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/siderolabs/gen v0.5.0 h1:Afdjx+zuZDf53eH5DB+E+T2JeCwBXGinV66A6osLgQI= +github.com/siderolabs/gen v0.5.0/go.mod h1:1GUMBNliW98Xeq8GPQeVMYqQE09LFItE8enR3wgMh3Q= +github.com/siderolabs/go-blockdevice/v2 v2.0.3 h1:IEgDqd3H3gPphahrdvfAzU8RmD4r5eQdWC+vgFQQoEg= +github.com/siderolabs/go-blockdevice/v2 v2.0.3/go.mod h1:74htzCV913UzaLZ4H+NBXkwWlYnBJIq5m/379ZEcu8w= +github.com/siderolabs/go-cmd v0.1.1 h1:nTouZUSxLeiiEe7hFexSVvaTsY/3O8k1s08BxPRrsps= +github.com/siderolabs/go-cmd v0.1.1/go.mod h1:6hY0JG34LxEEwYE8aH2iIHkHX/ir12VRLqfwAf2yJIY= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/orbit/changes/22047-linux-key-escrow b/orbit/changes/22047-linux-key-escrow new file mode 100644 index 000000000000..d8a3daa001a2 --- /dev/null +++ b/orbit/changes/22047-linux-key-escrow @@ -0,0 +1 @@ +* added functionality to support linux disk encryption key escrow including end user prompts and LUKS key management \ No newline at end of file diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index cf77cd197199..1be45d7cfb5a 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -26,6 +26,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/installer" "github.com/fleetdm/fleet/v4/orbit/pkg/keystore" "github.com/fleetdm/fleet/v4/orbit/pkg/logging" + "github.com/fleetdm/fleet/v4/orbit/pkg/luks" "github.com/fleetdm/fleet/v4/orbit/pkg/osquery" "github.com/fleetdm/fleet/v4/orbit/pkg/osservice" "github.com/fleetdm/fleet/v4/orbit/pkg/platform" @@ -38,6 +39,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/update" "github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore" "github.com/fleetdm/fleet/v4/orbit/pkg/user" + "github.com/fleetdm/fleet/v4/orbit/pkg/zenity" "github.com/fleetdm/fleet/v4/pkg/certificate" "github.com/fleetdm/fleet/v4/pkg/file" retrypkg "github.com/fleetdm/fleet/v4/pkg/retry" @@ -935,6 +937,8 @@ func main() { case "windows": orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMEnrollmentFetcherMiddleware(windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient)) orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMBitlockerFetcherMiddleware(windowsMDMBitlockerCommandFrequency, orbitClient)) + case "linux": + orbitClient.RegisterConfigReceiver(luks.New(orbitClient, zenity.New())) } flagUpdateReceiver := update.NewFlagReceiver(orbitClient.TriggerOrbitRestart, update.FlagUpdateOptions{ diff --git a/orbit/pkg/dialog/dialog.go b/orbit/pkg/dialog/dialog.go new file mode 100644 index 000000000000..362c77b0b691 --- /dev/null +++ b/orbit/pkg/dialog/dialog.go @@ -0,0 +1,66 @@ +package dialog + +import ( + "context" + "errors" + "time" +) + +var ( + // ErrCanceled is returned when the dialog is canceled by the cancel button. + ErrCanceled = errors.New("dialog canceled") + // ErrTimeout is returned when the dialog is automatically closed due to a timeout. + ErrTimeout = errors.New("dialog timed out") + // ErrUnknown is returned when an unknown error occurs. + ErrUnknown = errors.New("unknown error") +) + +// Dialog represents a UI dialog that can be displayed to the end user +// on a host +type Dialog interface { + // ShowEntry displays a dialog that accepts end user input. It returns the entered + // text or errors ErrCanceled, ErrTimeout, or ErrUnknown. + ShowEntry(ctx context.Context, opts EntryOptions) ([]byte, error) + // ShowInfo displays a dialog that displays information. It returns an error if the dialog + // could not be displayed. + ShowInfo(ctx context.Context, opts InfoOptions) error + // Progress displays a dialog that shows progress. It waits until the + // context is cancelled. + ShowProgress(ctx context.Context, opts ProgressOptions) error +} + +// EntryOptions represents options for a dialog that accepts end user input. +type EntryOptions struct { + // Title sets the title of the dialog. + Title string + + // Text sets the text of the dialog. + Text string + + // HideText hides the text entered by the user. + HideText bool + + // TimeOut sets the time in seconds before the dialog is automatically closed. + TimeOut time.Duration +} + +// InfoOptions represents options for a dialog that displays information. +type InfoOptions struct { + // Title sets the title of the dialog. + Title string + + // Text sets the text of the dialog. + Text string + + // Timeout sets the time in seconds before the dialog is automatically closed. + TimeOut time.Duration +} + +// ProgressOptions represents options for a dialog that shows progress. +type ProgressOptions struct { + // Title sets the title of the dialog. + Title string + + // Text sets the text of the dialog. + Text string +} diff --git a/orbit/pkg/execuser/execuser.go b/orbit/pkg/execuser/execuser.go index 5dc188ea99d8..5d4aaa353fef 100644 --- a/orbit/pkg/execuser/execuser.go +++ b/orbit/pkg/execuser/execuser.go @@ -2,6 +2,8 @@ // SYSTEM service on Windows) as the current login user. package execuser +import "context" + type eopts struct { env [][2]string args [][2]string @@ -19,10 +21,6 @@ func WithEnv(name, value string) Option { } // WithArg sets command line arguments for the application. -// -// TODO: for now CLI arguments are only used by the darwin -// implementation, just because it's the only platform that needs -// them. func WithArg(name, value string) Option { return func(a *eopts) { a.args = append(a.args, [2]string{name, value}) @@ -40,3 +38,27 @@ func Run(path string, opts ...Option) (lastLogs string, err error) { } return run(path, o) } + +// RunWithOutput runs an application as the current login user and returns its output. +// It assumes the caller is running with high privileges (root on UNIX). +// +// It blocks until the child process exits. +// Non ExitError errors return with a -1 exitCode. +func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, err error) { + var o eopts + for _, fn := range opts { + fn(&o) + } + return runWithOutput(path, o) +} + +// RunWithWait runs an application as the current login user and waits for it to finish +// or to be canceled by the context. Canceling the context will not return an error. +// It assumes the caller is running with high privileges (root on UNIX). +func RunWithWait(ctx context.Context, path string, opts ...Option) error { + var o eopts + for _, fn := range opts { + fn(&o) + } + return runWithWait(ctx, path, o) +} diff --git a/orbit/pkg/execuser/execuser_darwin.go b/orbit/pkg/execuser/execuser_darwin.go index 7902b2c761b2..ca92601ba980 100644 --- a/orbit/pkg/execuser/execuser_darwin.go +++ b/orbit/pkg/execuser/execuser_darwin.go @@ -1,6 +1,8 @@ package execuser import ( + "context" + "errors" "fmt" "io" "os" @@ -47,3 +49,11 @@ func run(path string, opts eopts) (lastLogs string, err error) { } return tw.String(), nil } + +func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) { + return nil, 0, errors.New("not implemented") +} + +func runWithWait(ctx context.Context, path string, opts eopts) error { + return errors.New("not implemented") +} diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go index 3ed91d7a62ec..1e9614d01be8 100644 --- a/orbit/pkg/execuser/execuser_linux.go +++ b/orbit/pkg/execuser/execuser_linux.go @@ -3,6 +3,7 @@ package execuser import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -18,9 +19,96 @@ import ( // run uses sudo to run the given path as login user. func run(path string, opts eopts) (lastLogs string, err error) { + args, err := getUserAndDisplayArgs(path, opts) + if err != nil { + return "", fmt.Errorf("get args: %w", err) + } + + args = append(args, + // Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH. + // + // Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to + // keep this to support older versions of Fleet Desktop. + fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")), + path, + ) + + cmd := exec.Command("sudo", args...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + log.Printf("cmd=%s", cmd.String()) + + if err := cmd.Start(); err != nil { + return "", fmt.Errorf("open path %q: %w", path, err) + } + return "", nil +} + +// run uses sudo to run the given path as login user and waits for the process to finish. +func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) { + args, err := getUserAndDisplayArgs(path, opts) + if err != nil { + return nil, -1, fmt.Errorf("get args: %w", err) + } + + args = append(args, path) + + if len(opts.args) > 0 { + for _, arg := range opts.args { + args = append(args, arg[0], arg[1]) + } + } + + cmd := exec.Command("sudo", args...) + log.Printf("cmd=%s", cmd.String()) + + output, err = cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + return output, exitCode, fmt.Errorf("%q exited with code %d: %w", path, exitCode, err) + } + return output, -1, fmt.Errorf("%q error: %w", path, err) + } + + return output, exitCode, nil +} + +func runWithWait(ctx context.Context, path string, opts eopts) error { + args, err := getUserAndDisplayArgs(path, opts) + if err != nil { + return fmt.Errorf("get args: %w", err) + } + + args = append(args, path) + + if len(opts.args) > 0 { + for _, arg := range opts.args { + args = append(args, arg[0], arg[1]) + } + } + + cmd := exec.CommandContext(ctx, "sudo", args...) + log.Printf("cmd=%s", cmd.String()) + + if err := cmd.Start(); err != nil { + return fmt.Errorf("cmd start %q: %w", path, err) + } + + if err := cmd.Wait(); err != nil { + if errors.Is(ctx.Err(), context.Canceled) { + return nil + } + return fmt.Errorf("cmd wait %q: %w", path, err) + } + + return nil +} + +func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) { user, err := getLoginUID() if err != nil { - return "", fmt.Errorf("get user: %w", err) + return nil, fmt.Errorf("get user: %w", err) } // TODO(lucas): Default to display :0 if user DISPLAY environment variable @@ -68,23 +156,9 @@ func run(path string, opts eopts) (lastLogs string, err error) { // This is required for Ubuntu 18, and not required for Ubuntu 21/22 // (because it's already part of the user). fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id), - // Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH. - // - // Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to - // keep this to support older versions of Fleet Desktop. - fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")), - path, ) - cmd := exec.Command("sudo", args...) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - log.Printf("cmd=%s", cmd.String()) - - if err := cmd.Start(); err != nil { - return "", fmt.Errorf("open path %q: %w", path, err) - } - return "", nil + return args, nil } type user struct { diff --git a/orbit/pkg/execuser/execuser_windows.go b/orbit/pkg/execuser/execuser_windows.go index 90e274b7a32d..f3bd58038db8 100644 --- a/orbit/pkg/execuser/execuser_windows.go +++ b/orbit/pkg/execuser/execuser_windows.go @@ -6,6 +6,7 @@ package execuser // To view what was modified/added, you can use the execuser_windows_diff.sh script. import ( + "context" "errors" "fmt" "os" @@ -117,6 +118,14 @@ func run(path string, opts eopts) (lastLogs string, err error) { return "", startProcessAsCurrentUser(path, "", "") } +func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) { + return nil, 0, errors.New("not implemented") +} + +func runWithWait(ctx context.Context, path string, opts eopts) error { + return errors.New("not implemented") +} + // getCurrentUserSessionId will attempt to resolve // the session ID of the user currently active on // the system. diff --git a/orbit/pkg/luks/luks.go b/orbit/pkg/luks/luks.go new file mode 100644 index 000000000000..6376b24eaa83 --- /dev/null +++ b/orbit/pkg/luks/luks.go @@ -0,0 +1,37 @@ +package luks + +import ( + "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" +) + +type KeyEscrower interface { + SendLinuxKeyEscrowResponse(LuksResponse) error +} + +type LuksRunner struct { + escrower KeyEscrower + notifier dialog.Dialog +} + +type LuksResponse struct { + // Passphrase is a newly created passphrase generated by fleetd for securing the LUKS volume. + // This passphrase will be securely escrowed to the server. + Passphrase string + + // KeySlot specifies the LUKS key slot where this new passphrase was created. + // It is currently not used, but may be useful in the future for passphrase rotation. + KeySlot *uint + + // Salt is the salt used to generate the LUKS key. + Salt string + + // Err is the error message that occurred during the escrow process. + Err string +} + +func New(escrower KeyEscrower, notifier dialog.Dialog) *LuksRunner { + return &LuksRunner{ + escrower: escrower, + notifier: notifier, + } +} diff --git a/orbit/pkg/luks/luks_linux.go b/orbit/pkg/luks/luks_linux.go new file mode 100644 index 000000000000..9c32307a500e --- /dev/null +++ b/orbit/pkg/luks/luks_linux.go @@ -0,0 +1,294 @@ +//go:build linux + +package luks + +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "math/big" + "os/exec" + "regexp" + "time" + + "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" + "github.com/fleetdm/fleet/v4/orbit/pkg/lvm" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/rs/zerolog/log" + "github.com/siderolabs/go-blockdevice/v2/encryption" + luksdevice "github.com/siderolabs/go-blockdevice/v2/encryption/luks" +) + +const ( + entryDialogTitle = "Enter disk encryption passphrase" + entryDialogText = "Passphrase:" + retryEntryDialogText = "Passphrase incorrect. Please try again." + infoFailedTitle = "Encryption key escrow" + infoFailedText = "Failed to escrow key. Please try again later." + infoSuccessTitle = "Encryption key escrow" + infoSuccessText = "Key escrowed successfully." + maxKeySlots = 8 + userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase +) + +var ErrKeySlotFull = regexp.MustCompile(`Key slot \d+ is full`) + +func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { + ctx := context.Background() + + if !oc.Notifications.RunDiskEncryptionEscrow { + return nil + } + + devicePath, err := lvm.FindRootDisk() + if err != nil { + return fmt.Errorf("Failed to find LUKS Root Partition: %w", err) + } + + var response LuksResponse + key, keyslot, err := lr.getEscrowKey(ctx, devicePath) + if err != nil { + response.Err = err.Error() + } + + response.Passphrase = string(key) + response.KeySlot = keyslot + + if keyslot != nil { + salt, err := getSaltforKeySlot(ctx, devicePath, *keyslot) + if err != nil { + if err := removeKeySlot(ctx, devicePath, *keyslot); err != nil { + log.Error().Err(err).Msgf("failed to remove key slot %d", *keyslot) + } + return fmt.Errorf("Failed to get salt for key slot: %w", err) + } + response.Salt = salt + } + + if err := lr.escrower.SendLinuxKeyEscrowResponse(response); err != nil { + // If sending the response fails, remove the key slot + if keyslot != nil { + if err := removeKeySlot(ctx, devicePath, *keyslot); err != nil { + log.Error().Err(err).Msg("failed to remove key slot") + } + } + + // Show error in dialog + if err := lr.infoPrompt(ctx, infoFailedTitle, infoFailedText); err != nil { + log.Info().Err(err).Msg("failed to show failed escrow key dialog") + } + + return fmt.Errorf("escrower escrowKey err: %w", err) + } + + if response.Err != "" { + if err := lr.infoPrompt(ctx, infoFailedTitle, response.Err); err != nil { + log.Info().Err(err).Msg("failed to show response error dialog") + } + return fmt.Errorf("error getting linux escrow key: %s", response.Err) + } + + // Show success dialog + if err := lr.infoPrompt(ctx, infoSuccessTitle, infoSuccessText); err != nil { + log.Info().Err(err).Msg("failed to show success escrow key dialog") + } + + return nil +} + +func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]byte, *uint, error) { + // AESXTSPlain64Cipher is the default cipher used by ubuntu/kubuntu/fedora + device := luksdevice.New(luksdevice.AESXTSPlain64Cipher) + + // Prompt user for existing LUKS passphrase + passphrase, err := lr.entryPrompt(ctx, entryDialogTitle, entryDialogText) + if err != nil { + return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err) + } + + // Validate the passphrase + for { + valid, err := lr.passphraseIsValid(ctx, device, devicePath, passphrase, userKeySlot) + if err != nil { + return nil, nil, fmt.Errorf("Failed validating passphrase: %w", err) + } + + if valid { + break + } + + passphrase, err = lr.entryPrompt(ctx, entryDialogTitle, retryEntryDialogText) + if err != nil { + return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err) + } + } + + if len(passphrase) == 0 { + log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") + return nil, nil, nil + } + + escrowPassphrase, err := generateRandomPassphrase() + if err != nil { + return nil, nil, fmt.Errorf("Failed to generate random passphrase: %w", err) + } + + // Create a new key slot and error if all key slots are full + // Start at slot 1 as keySlot 0 is assumed to be the location of + // the user's passphrase + var keySlot uint = userKeySlot + 1 + for { + if keySlot == maxKeySlots { + return nil, nil, errors.New("all LUKS key slots are full") + } + + userKey := encryption.NewKey(userKeySlot, passphrase) + escrowKey := encryption.NewKey(int(keySlot), escrowPassphrase) // #nosec G115 + + if err := device.AddKey(ctx, devicePath, userKey, escrowKey); err != nil { + if ErrKeySlotFull.MatchString(err.Error()) { + keySlot++ + continue + } + return nil, nil, fmt.Errorf("Failed to add key: %w", err) + } + + break + } + + valid, err := lr.passphraseIsValid(ctx, device, devicePath, escrowPassphrase, keySlot) + if err != nil { + return nil, nil, fmt.Errorf("Error while validating escrow passphrase: %w", err) + } + + if !valid { + return nil, nil, errors.New("Failed to validate escrow passphrase") + } + + return escrowPassphrase, &keySlot, nil +} + +func (lr *LuksRunner) passphraseIsValid(ctx context.Context, device *luksdevice.LUKS, devicePath string, passphrase []byte, keyslot uint) (bool, error) { + if len(passphrase) == 0 { + return false, nil + } + + valid, err := device.CheckKey(ctx, devicePath, encryption.NewKey(int(keyslot), passphrase)) // #nosec G115 + if err != nil { + return false, fmt.Errorf("Error validating passphrase: %w", err) + } + + return valid, nil +} + +// generateRandomPassphrase generates a random passphrase with 32 characters +// in the format XXXX-XXXX-XXXX-XXXX where X is a random character from the +// set [0-9A-Za-z]. +func generateRandomPassphrase() ([]byte, error) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + const length = 35 // 32 characters + 3 dashes + passphrase := make([]byte, length) + + for i := 0; i < length; i++ { + // Insert dashes at positions 8, 17, and 26 + if i == 8 || i == 17 || i == 26 { + passphrase[i] = '-' + continue + } + + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) + if err != nil { + return nil, err + } + passphrase[i] = chars[num.Int64()] + } + + return passphrase, nil +} + +func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]byte, error) { + passphrase, err := lr.notifier.ShowEntry(ctx, dialog.EntryOptions{ + Title: title, + Text: text, + HideText: true, + TimeOut: 1 * time.Minute, + }) + if err != nil { + switch err { + case dialog.ErrCanceled: + log.Debug().Msg("end user canceled key escrow dialog") + return nil, nil + case dialog.ErrTimeout: + log.Debug().Msg("key escrow dialog timed out") + return nil, nil + case dialog.ErrUnknown: + return nil, err + default: + return nil, err + } + } + + return passphrase, nil +} + +func (lr *LuksRunner) infoPrompt(ctx context.Context, title, text string) error { + err := lr.notifier.ShowInfo(ctx, dialog.InfoOptions{ + Title: title, + Text: text, + TimeOut: 30 * time.Second, + }) + if err != nil { + switch err { + case dialog.ErrTimeout: + log.Debug().Msg("successPrompt timed out") + return nil + default: + return err + } + } + + return nil +} + +type LuksDump struct { + Keyslots map[string]Keyslot `json:"keyslots"` +} + +type Keyslot struct { + KDF KDF `json:"kdf"` +} + +type KDF struct { + Salt string `json:"salt"` +} + +func getSaltforKeySlot(ctx context.Context, devicePath string, keySlot uint) (string, error) { + cmd := exec.CommandContext(ctx, "cryptsetup", "luksDump", "--dump-json-metadata", devicePath) + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("Failed to run cryptsetup luksDump: %w", err) + } + + var dump LuksDump + if err := json.Unmarshal(output, &dump); err != nil { + return "", fmt.Errorf("Failed to unmarshal luksDump output: %w", err) + } + + slot, ok := dump.Keyslots[fmt.Sprintf("%d", keySlot)] + if !ok { + return "", errors.New("key slot not found") + } + + return slot.KDF.Salt, nil +} + +func removeKeySlot(ctx context.Context, devicePath string, keySlot uint) error { + cmd := exec.CommandContext(ctx, "cryptsetup", "luksKillSlot", devicePath, fmt.Sprintf("%d", keySlot)) // #nosec G204 + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to run cryptsetup luksKillSlot: %w", err) + } + + return nil +} diff --git a/orbit/pkg/luks/luks_stub.go b/orbit/pkg/luks/luks_stub.go new file mode 100644 index 000000000000..4358df26c744 --- /dev/null +++ b/orbit/pkg/luks/luks_stub.go @@ -0,0 +1,13 @@ +//go:build !linux +// +build !linux + +package luks + +import ( + "github.com/fleetdm/fleet/v4/server/fleet" +) + +// Run is a placeholder method for non-Linux builds. +func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { + return nil +} diff --git a/orbit/pkg/lvm/lvm.go b/orbit/pkg/lvm/lvm.go new file mode 100644 index 000000000000..c662d80d78fd --- /dev/null +++ b/orbit/pkg/lvm/lvm.go @@ -0,0 +1,104 @@ +package lvm + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os/exec" +) + +type BlockDevice struct { + Name string `json:"name"` + Type string `json:"type"` + Mountpoints []string `json:"mountpoints"` + Children []BlockDevice `json:"children,omitempty"` +} + +// FindRootDisk finds the physical partition that +// contains the root filesystem mounted at "/" on a +// LVM Linux volume. +func FindRootDisk() (string, error) { + cmd := exec.Command("lsblk", "--json") + var out bytes.Buffer + cmd.Stdout = &out + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to run lsblk: %w", err) + } + + return rootDiskFromJson(out) +} + +func rootDiskFromJson(input bytes.Buffer) (string, error) { + var data struct { + Blockdevices []BlockDevice `json:"blockdevices"` + } + + if err := json.Unmarshal(input.Bytes(), &data); err != nil { + return "", fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + // Find the root partition mounted at "/" + rootPartition := findRootPartition(data.Blockdevices) + if rootPartition == nil { + return "", errors.New("root partition not found") + } + + // Trace up to the nearest parent partition of type "part" (partition) + physicalPartition := findParentPartitionOfTypePart(data.Blockdevices, rootPartition) + if physicalPartition == nil { + return "", errors.New("physical partition of type 'part' not found") + } + + return fmt.Sprintf("/dev/%s", physicalPartition.Name), nil +} + +// findRootPartition recursively searches for the partition +// mounted at "/" within the device tree. +func findRootPartition(devices []BlockDevice) *BlockDevice { + for _, device := range devices { + if result := searchForRoot(device); result != nil { + return result + } + } + return nil +} + +// searchForRoot recursively checks each device and its children +// to find the one mounted at "/". +func searchForRoot(device BlockDevice) *BlockDevice { + for _, mountpoint := range device.Mountpoints { + if mountpoint == "/" { + return &device + } + } + for _, child := range device.Children { + if result := searchForRoot(child); result != nil { + return result + } + } + return nil +} + +// findParentPartitionOfTypePart traverses upwards from the given device to find the nearest "part" type parent. +func findParentPartitionOfTypePart(devices []BlockDevice, target *BlockDevice) *BlockDevice { + var queue []BlockDevice + queue = append(queue, devices...) + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + for _, child := range current.Children { + if child.Name == target.Name { + if current.Type == "part" { + return ¤t + } + return findParentPartitionOfTypePart(devices, ¤t) + } + queue = append(queue, child) + } + } + return nil +} diff --git a/orbit/pkg/lvm/lvm_test.go b/orbit/pkg/lvm/lvm_test.go new file mode 100644 index 000000000000..73058caf09c8 --- /dev/null +++ b/orbit/pkg/lvm/lvm_test.go @@ -0,0 +1,344 @@ +package lvm + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +// sample from real LUKS encrypted Ubuntu disk +var testJsonUbuntu = `{ + "blockdevices": [ + { + "name": "loop0", + "maj:min": "7:0", + "rm": false, + "size": "4K", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/bare/5" + ] + },{ + "name": "loop1", + "maj:min": "7:1", + "rm": false, + "size": "74.3M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/core22/1564" + ] + },{ + "name": "loop2", + "maj:min": "7:2", + "rm": false, + "size": "73.9M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/core22/1663" + ] + },{ + "name": "loop3", + "maj:min": "7:3", + "rm": false, + "size": "269.8M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/firefox/4793" + ] + },{ + "name": "loop4", + "maj:min": "7:4", + "rm": false, + "size": "10.7M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/firmware-updater/127" + ] + },{ + "name": "loop5", + "maj:min": "7:5", + "rm": false, + "size": "11.1M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/firmware-updater/147" + ] + },{ + "name": "loop6", + "maj:min": "7:6", + "rm": false, + "size": "505.1M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/gnome-42-2204/176" + ] + },{ + "name": "loop7", + "maj:min": "7:7", + "rm": false, + "size": "91.7M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/gtk-common-themes/1535" + ] + },{ + "name": "loop8", + "maj:min": "7:8", + "rm": false, + "size": "10.7M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/snap-store/1218" + ] + },{ + "name": "loop9", + "maj:min": "7:9", + "rm": false, + "size": "10.5M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/snap-store/1173" + ] + },{ + "name": "loop10", + "maj:min": "7:10", + "rm": false, + "size": "38.8M", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/snapd/21759" + ] + },{ + "name": "loop11", + "maj:min": "7:11", + "rm": false, + "size": "500K", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/snapd-desktop-integration/178" + ] + },{ + "name": "loop12", + "maj:min": "7:12", + "rm": false, + "size": "568K", + "ro": true, + "type": "loop", + "mountpoints": [ + "/snap/snapd-desktop-integration/253" + ] + },{ + "name": "nvme0n1", + "maj:min": "259:0", + "rm": false, + "size": "476.9G", + "ro": false, + "type": "disk", + "mountpoints": [ + null + ], + "children": [ + { + "name": "nvme0n1p1", + "maj:min": "259:1", + "rm": false, + "size": "1G", + "ro": false, + "type": "part", + "mountpoints": [ + "/boot/efi" + ] + },{ + "name": "nvme0n1p2", + "maj:min": "259:2", + "rm": false, + "size": "2G", + "ro": false, + "type": "part", + "mountpoints": [ + "/boot" + ] + },{ + "name": "nvme0n1p3", + "maj:min": "259:3", + "rm": false, + "size": "473.9G", + "ro": false, + "type": "part", + "mountpoints": [ + null + ], + "children": [ + { + "name": "dm_crypt-0", + "maj:min": "252:0", + "rm": false, + "size": "473.9G", + "ro": false, + "type": "crypt", + "mountpoints": [ + null + ], + "children": [ + { + "name": "ubuntu--vg-ubuntu--lv", + "maj:min": "252:1", + "rm": false, + "size": "473.9G", + "ro": false, + "type": "lvm", + "mountpoints": [ + "/" + ] + } + ] + } + ] + } + ] + } + ] +}` + +var testJsonFedora = `{ + "blockdevices": [ + { + "name": "sr0", + "maj:min": "11:0", + "rm": true, + "size": "2.1G", + "ro": false, + "type": "rom", + "mountpoints": [ + "/run/media/luk/Fedora-WS-Live-40-1-14" + ] + },{ + "name": "zram0", + "maj:min": "252:0", + "rm": false, + "size": "1.9G", + "ro": false, + "type": "disk", + "mountpoints": [ + "[SWAP]" + ] + },{ + "name": "nvme0n1", + "maj:min": "259:0", + "rm": false, + "size": "20G", + "ro": false, + "type": "disk", + "mountpoints": [ + null + ], + "children": [ + { + "name": "nvme0n1p1", + "maj:min": "259:1", + "rm": false, + "size": "600M", + "ro": false, + "type": "part", + "mountpoints": [ + "/boot/efi" + ] + },{ + "name": "nvme0n1p2", + "maj:min": "259:2", + "rm": false, + "size": "1G", + "ro": false, + "type": "part", + "mountpoints": [ + "/boot" + ] + },{ + "name": "nvme0n1p3", + "maj:min": "259:3", + "rm": false, + "size": "18.4G", + "ro": false, + "type": "part", + "mountpoints": [ + null + ], + "children": [ + { + "name": "luks-21fc9b67-752e-42fb-83bb-8c92864382e9", + "maj:min": "253:0", + "rm": false, + "size": "18.4G", + "ro": false, + "type": "crypt", + "mountpoints": [ + "/home", "/" + ] + } + ] + } + ] + } + ] +}` + +func TestFindRootDisk(t *testing.T) { + var input bytes.Buffer + _, err := input.WriteString(testJsonUbuntu) + assert.NoError(t, err) + + output, err := rootDiskFromJson(input) + assert.NoError(t, err) + assert.Equal(t, "/dev/nvme0n1p3", output) + + input = bytes.Buffer{} + _, err = input.WriteString(testJsonFedora) + assert.NoError(t, err) + + output, err = rootDiskFromJson(input) + assert.NoError(t, err) + assert.Equal(t, "/dev/nvme0n1p3", output) +} + +func TestErrorNoMountPoint(t *testing.T) { + var input bytes.Buffer + _, err := input.WriteString(`{"blockdevices": [{"name": "nvme0n1", "mountpoints": [null]}]}`) + assert.NoError(t, err) + + output, err := rootDiskFromJson(input) + assert.Error(t, err) + assert.Empty(t, output) +} + +func TestErrorNoRootPartition(t *testing.T) { + var input bytes.Buffer + _, err := input.WriteString(`{"blockdevices": [{"name": "nvme0n1", "mountpoints": ["/boot"]}]}`) + assert.NoError(t, err) + + output, err := rootDiskFromJson(input) + assert.Error(t, err) + assert.Empty(t, output) +} + +func TestErrorInvalidJson(t *testing.T) { + var input bytes.Buffer + _, err := input.WriteString(`{`) + assert.NoError(t, err) + + output, err := rootDiskFromJson(input) + assert.Error(t, err) + assert.Empty(t, output) +} diff --git a/orbit/pkg/zenity/zenity.go b/orbit/pkg/zenity/zenity.go new file mode 100644 index 000000000000..bd51d214f82b --- /dev/null +++ b/orbit/pkg/zenity/zenity.go @@ -0,0 +1,140 @@ +package zenity + +import ( + "bytes" + "context" + "fmt" + + "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" + "github.com/fleetdm/fleet/v4/orbit/pkg/execuser" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" +) + +type Zenity struct { + // cmdWithOutput can be set in tests to mock execution of the dialog. + cmdWithOutput func(ctx context.Context, args ...string) ([]byte, int, error) + // cmdWithWait can be set in tests to mock execution of the dialog. + cmdWithWait func(ctx context.Context, args ...string) error +} + +// New creates a new Zenity dialog instance for zenity v4 on Linux. +// Zenity implements the Dialog interface. +func New() *Zenity { + return &Zenity{ + cmdWithOutput: execCmdWithOutput, + cmdWithWait: execCmdWithWait, + } +} + +// ShowEntry displays an dialog that accepts end user input. It returns the entered +// text or errors ErrCanceled, ErrTimeout, or ErrUnknown. +func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byte, error) { + args := []string{"--entry"} + if opts.Title != "" { + args = append(args, fmt.Sprintf("--title=%s", opts.Title)) + } + if opts.Text != "" { + args = append(args, fmt.Sprintf("--text=%s", opts.Text)) + } + if opts.HideText { + args = append(args, "--hide-text") + } + if opts.TimeOut > 0 { + args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds()))) + } + + output, statusCode, err := z.cmdWithOutput(ctx, args...) + if err != nil { + switch statusCode { + case 1: + return nil, ctxerr.Wrap(ctx, dialog.ErrCanceled) + case 5: + return nil, ctxerr.Wrap(ctx, dialog.ErrTimeout) + default: + return nil, ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error()) + } + } + + return output, nil +} + +// ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown. +func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error { + args := []string{"--info"} + if opts.Title != "" { + args = append(args, fmt.Sprintf("--title=%s", opts.Title)) + } + if opts.Text != "" { + args = append(args, fmt.Sprintf("--text=%s", opts.Text)) + } + if opts.TimeOut > 0 { + args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds()))) + } + + _, statusCode, err := z.cmdWithOutput(ctx, args...) + if err != nil { + switch statusCode { + case 5: + return ctxerr.Wrap(ctx, dialog.ErrTimeout) + default: + return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error()) + } + } + + return nil +} + +// ShowProgress starts a Zenity progress dialog with the given options. +// This function is designed to block until the provided context is canceled. +// It is intended to be used within a separate goroutine to avoid blocking +// the main execution flow. +// +// If the context is already canceled, the function will return immediately. +// +// Use this function for cases where a progress dialog is needed to run +// alongside other operations, with explicit cancellation or termination. +func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions) error { + args := []string{"--progress"} + if opts.Title != "" { + args = append(args, fmt.Sprintf("--title=%s", opts.Title)) + } + if opts.Text != "" { + args = append(args, fmt.Sprintf("--text=%s", opts.Text)) + } + + // --pulsate shows a pulsating progress bar + args = append(args, "--pulsate") + + // --no-cancel disables the cancel button + args = append(args, "--no-cancel") + + err := z.cmdWithWait(ctx, args...) + if err != nil { + return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error()) + } + + return nil +} + +func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error) { + var opts []execuser.Option + for _, arg := range args { + opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args + } + + output, exitCode, err := execuser.RunWithOutput("zenity", opts...) + + // Trim the newline from zenity output + output = bytes.TrimSuffix(output, []byte("\n")) + + return output, exitCode, err +} + +func execCmdWithWait(ctx context.Context, args ...string) error { + var opts []execuser.Option + for _, arg := range args { + opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args + } + + return execuser.RunWithWait(ctx, "zenity", opts...) +} diff --git a/orbit/pkg/zenity/zenity_test.go b/orbit/pkg/zenity/zenity_test.go new file mode 100644 index 000000000000..5d57f52d91ff --- /dev/null +++ b/orbit/pkg/zenity/zenity_test.go @@ -0,0 +1,268 @@ +package zenity + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" + "github.com/stretchr/testify/require" + "github.com/tj/assert" +) + +type mockExecCmd struct { + output []byte + exitCode int + capturedArgs []string + waitDuration time.Duration +} + +// MockCommandContext simulates exec.CommandContext and captures arguments +func (m *mockExecCmd) runWithOutput(ctx context.Context, args ...string) ([]byte, int, error) { + m.capturedArgs = append(m.capturedArgs, args...) + + if m.exitCode != 0 { + return nil, m.exitCode, &exec.ExitError{} + } + + return m.output, m.exitCode, nil +} + +func (m *mockExecCmd) runWithWait(ctx context.Context, args ...string) error { + m.capturedArgs = append(m.capturedArgs, args...) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(m.waitDuration): + + } + + return nil +} + +func TestShowEntryArgs(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + opts dialog.EntryOptions + expectedArgs []string + }{ + { + name: "Basic Entry", + opts: dialog.EntryOptions{ + Title: "A Title", + Text: "Some text", + }, + expectedArgs: []string{"--entry", "--title=A Title", "--text=Some text"}, + }, + { + name: "All Options", + opts: dialog.EntryOptions{ + Title: "Another Title", + Text: "Some more text", + HideText: true, + TimeOut: 1 * time.Minute, + }, + expectedArgs: []string{"--entry", "--title=Another Title", "--text=Some more text", "--hide-text", "--timeout=60"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + mock := &mockExecCmd{ + output: []byte("some output"), + } + z := &Zenity{ + cmdWithOutput: mock.runWithOutput, + } + output, err := z.ShowEntry(ctx, tt.opts) + assert.NoError(t, err) + assert.Equal(t, tt.expectedArgs, mock.capturedArgs) + assert.Equal(t, []byte("some output"), output) + }) + } +} + +func TestShowEntryError(t *testing.T) { + ctx := context.Background() + + testcases := []struct { + name string + exitCode int + expectedErr error + }{ + { + name: "Dialog Cancelled", + exitCode: 1, + expectedErr: dialog.ErrCanceled, + }, + { + name: "Dialog Timed Out", + exitCode: 5, + expectedErr: dialog.ErrTimeout, + }, + { + name: "Unknown Error", + exitCode: 99, + expectedErr: dialog.ErrUnknown, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + mock := &mockExecCmd{ + exitCode: tt.exitCode, + } + z := &Zenity{ + cmdWithOutput: mock.runWithOutput, + } + output, err := z.ShowEntry(ctx, dialog.EntryOptions{}) + require.ErrorIs(t, err, tt.expectedErr) + assert.Nil(t, output) + }) + } +} + +func TestShowEntrySuccess(t *testing.T) { + ctx := context.Background() + + mock := &mockExecCmd{ + output: []byte("some output"), + } + z := &Zenity{ + cmdWithOutput: mock.runWithOutput, + } + output, err := z.ShowEntry(ctx, dialog.EntryOptions{}) + assert.NoError(t, err) + assert.Equal(t, []byte("some output"), output) +} + +func TestShowInfoArgs(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + opts dialog.InfoOptions + expectedArgs []string + }{ + { + name: "Basic Entry", + opts: dialog.InfoOptions{}, + expectedArgs: []string{"--info"}, + }, + { + name: "All Options", + opts: dialog.InfoOptions{ + Title: "Another Title", + Text: "Some more text", + TimeOut: 1 * time.Minute, + }, + expectedArgs: []string{"--info", "--title=Another Title", "--text=Some more text", "--timeout=60"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + mock := &mockExecCmd{} + z := &Zenity{ + cmdWithOutput: mock.runWithOutput, + } + err := z.ShowInfo(ctx, tt.opts) + assert.NoError(t, err) + assert.Equal(t, tt.expectedArgs, mock.capturedArgs) + }) + } +} + +func TestShowInfoError(t *testing.T) { + ctx := context.Background() + + testcases := []struct { + name string + exitCode int + expectedErr error + }{ + { + name: "Dialog Timed Out", + exitCode: 5, + expectedErr: dialog.ErrTimeout, + }, + { + name: "Unknown Error", + exitCode: 99, + expectedErr: dialog.ErrUnknown, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + mock := &mockExecCmd{ + exitCode: tt.exitCode, + } + z := &Zenity{ + cmdWithOutput: mock.runWithOutput, + } + err := z.ShowInfo(ctx, dialog.InfoOptions{}) + require.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestProgressArgs(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + opts dialog.ProgressOptions + expectedArgs []string + }{ + { + name: "Basic Entry", + opts: dialog.ProgressOptions{ + Title: "A Title", + Text: "Some text", + }, + expectedArgs: []string{"--progress", "--title=A Title", "--text=Some text", "--pulsate", "--no-cancel"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + mock := &mockExecCmd{} + z := &Zenity{ + cmdWithWait: mock.runWithWait, + } + err := z.ShowProgress(ctx, tt.opts) + assert.NoError(t, err) + assert.Equal(t, tt.expectedArgs, mock.capturedArgs) + }) + } +} + +func TestProgressKillOnCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + mock := &mockExecCmd{ + waitDuration: 5 * time.Second, + } + z := &Zenity{ + cmdWithWait: mock.runWithWait, + } + + done := make(chan struct{}) + start := time.Now() + + go func() { + _ = z.ShowProgress(ctx, dialog.ProgressOptions{}) + close(done) + }() + + time.Sleep(100 * time.Millisecond) + cancel() + <-done + + assert.True(t, time.Since(start) < 5*time.Second) +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 1c74569817b7..b2f7f3a650ed 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3923,7 +3923,7 @@ func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) var key fleet.HostDiskEncryptionKey err := sqlx.GetContext(ctx, ds.reader(ctx), &key, ` SELECT - host_id, base64_encrypted, decryptable, updated_at + host_id, base64_encrypted, decryptable, updated_at, client_error FROM host_disk_encryption_keys WHERE host_id = ?`, hostID) diff --git a/server/datastore/mysql/linux_mdm.go b/server/datastore/mysql/linux_mdm.go new file mode 100644 index 000000000000..2cd88843efd2 --- /dev/null +++ b/server/datastore/mysql/linux_mdm.go @@ -0,0 +1,69 @@ +package mysql + +import ( + "context" + "fmt" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/jmoiron/sqlx" +) + +func (ds *Datastore) GetLinuxDiskEncryptionSummary(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) { + var args []interface{} + var teamFilter string + if teamID != nil { + teamFilter = "AND h.team_id = ?" + args = append(args, *teamID) + } else { + teamFilter = "AND h.team_id IS NULL" + } + + stmt := fmt.Sprintf(`SELECT + CASE WHEN hdek.base64_encrypted IS NOT NULL + AND hdek.base64_encrypted != '' + AND hdek.client_error = '' THEN + 'verified' + WHEN hdek.client_error IS NOT NULL + AND hdek.client_error != '' THEN + 'failed' + WHEN hdek.base64_encrypted IS NULL + OR (hdek.base64_encrypted = '' + AND hdek.client_error = '') THEN + 'action_required' + END AS status, + COUNT(h.id) AS host_count + FROM + hosts h + LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id + WHERE + (h.os_version LIKE '%%fedora%%' + OR h.platform LIKE 'ubuntu') + %s + GROUP BY + status`, teamFilter) + + type countRow struct { + Status string `db:"status"` + HostCount uint `db:"host_count"` + } + + var counts []countRow + summary := fleet.MDMLinuxDiskEncryptionSummary{} + + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...); err != nil { + return summary, err + } + + for _, count := range counts { + switch count.Status { + case "verified": + summary.Verified = count.HostCount + case "action_required": + summary.ActionRequired = count.HostCount + case "failed": + summary.Failed = count.HostCount + } + } + + return summary, nil +} diff --git a/server/datastore/mysql/linux_mdm_test.go b/server/datastore/mysql/linux_mdm_test.go new file mode 100644 index 000000000000..d0cecb405bea --- /dev/null +++ b/server/datastore/mysql/linux_mdm_test.go @@ -0,0 +1,146 @@ +package mysql + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/test" + "github.com/stretchr/testify/require" +) + +func TestLinuxDiskEncryptionSummary(t *testing.T) { + ds := CreateMySQLDS(t) + ctx := context.Background() + + // 5 new ubuntu hosts + var ubuntuHosts []*fleet.Host + for i := 0; i < 5; i++ { + h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", + fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), test.WithPlatform("ubuntu")) + ubuntuHosts = append(ubuntuHosts, h) + } + + // 5 new fedora hosts + var fedoraHosts []*fleet.Host + for i := 5; i < 10; i++ { + h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", + fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), + test.WithOSVersion("Fedora Linux 38.0.0"), test.WithPlatform("rhel")) + fedoraHosts = append(fedoraHosts, h) + } + + // 5 macos hosts + var macosHosts []*fleet.Host + for i := 10; i < 15; i++ { + h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", + fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), test.WithPlatform("darwin")) + macosHosts = append(macosHosts, h) + } + + // no teams tests ===== + summary, err := ds.GetLinuxDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + + require.Equal(t, uint(0), summary.Verified) + require.Equal(t, uint(10), summary.ActionRequired) + require.Equal(t, uint(0), summary.Failed) + + // Add disk encryption keys + + // ubuntu + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0].ID, "base64_encrypted", "", nil) + require.NoError(t, err) + // fedora + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, fedoraHosts[0].ID, "base64_encrypted", "", nil) + require.NoError(t, err) + // macos + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, macosHosts[0].ID, "base64_encrypted", "", nil) + require.NoError(t, err) + + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + + require.Equal(t, uint(2), summary.Verified) + require.Equal(t, uint(8), summary.ActionRequired) + require.Equal(t, uint(0), summary.Failed) + + // update ubuntu with key and client error + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0].ID, "base64_encrypted", "client error", nil) + require.NoError(t, err) + + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + + require.Equal(t, uint(1), summary.Verified) + require.Equal(t, uint(8), summary.ActionRequired) + require.Equal(t, uint(1), summary.Failed) + + // add ubuntu with no key and client error + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[1].ID, "", "client error", nil) + require.NoError(t, err) + + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + + require.Equal(t, uint(1), summary.Verified) + require.Equal(t, uint(7), summary.ActionRequired) + require.Equal(t, uint(2), summary.Failed) + + // move verified fedora host to team will remove existing key + team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) + require.NoError(t, err) + + err = ds.AddHostsToTeam(ctx, &team.ID, []uint{fedoraHosts[0].ID}) + require.NoError(t, err) + + // team summary + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, &team.ID) + require.NoError(t, err) + + require.Equal(t, uint(0), summary.Verified) + require.Equal(t, uint(1), summary.ActionRequired) + require.Equal(t, uint(0), summary.Failed) + + // no team summary + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + + require.Equal(t, uint(0), summary.Verified) + require.Equal(t, uint(7), summary.ActionRequired) + require.Equal(t, uint(2), summary.Failed) + + // move all hosts to team + for _, h := range ubuntuHosts { + err = ds.AddHostsToTeam(ctx, &team.ID, []uint{h.ID}) + require.NoError(t, err) + } + + for _, h := range fedoraHosts { + err = ds.AddHostsToTeam(ctx, &team.ID, []uint{h.ID}) + require.NoError(t, err) + } + + for _, h := range macosHosts { + err = ds.AddHostsToTeam(ctx, &team.ID, []uint{h.ID}) + require.NoError(t, err) + } + + // team summary + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, &team.ID) + require.NoError(t, err) + + require.Equal(t, uint(0), summary.Verified) + require.Equal(t, uint(10), summary.ActionRequired) + require.Equal(t, uint(0), summary.Failed) + + // no team summary + summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + + require.Equal(t, uint(0), summary.Verified) + require.Equal(t, uint(0), summary.ActionRequired) + require.Equal(t, uint(0), summary.Failed) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 6464b4b51c97..5979c7ade843 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1514,6 +1514,14 @@ type Datastore interface { // GetHostMDMProfileInstallStatus returns the status of the profile for the host. GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID string, profileUUID string) (MDMDeliveryStatus, error) + /////////////////////////////////////////////////////////////////////////////// + // Linux MDM + + // GetLinuxDiskEncryptionSummary summarizes the current state of Linux disk encryption on + // each Linux host in the specified team (or, if no team is specified, each host that is not assigned + // to any team). + GetLinuxDiskEncryptionSummary(ctx context.Context, teamID *uint) (MDMLinuxDiskEncryptionSummary, error) + /////////////////////////////////////////////////////////////////////////////// // MDM Commands diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 407af288f825..95ce9ee268e8 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -1179,6 +1179,7 @@ type HostDiskEncryptionKey struct { Decryptable *bool `json:"-" db:"decryptable"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` DecryptedValue string `json:"key" db:"-"` + ClientError string `json:"-" db:"client_error"` } // HostSoftwareInstalledPath represents where in the file system a software on a host was installed diff --git a/server/fleet/linux_mdm.go b/server/fleet/linux_mdm.go new file mode 100644 index 000000000000..7a9a5544e105 --- /dev/null +++ b/server/fleet/linux_mdm.go @@ -0,0 +1,7 @@ +package fleet + +type MDMLinuxDiskEncryptionSummary struct { + Verified uint `json:"verified"` + ActionRequired uint `json:"action_required"` + Failed uint `json:"failed"` +} diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 7f717eeb14ba..55e28bc7b945 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -298,6 +298,7 @@ type MDMCommandFilters struct { type MDMPlatformsCounts struct { MacOS uint `db:"macos" json:"macos"` Windows uint `db:"windows" json:"windows"` + Linux uint `db:"linux" json:"linux"` } type MDMDiskEncryptionSummary struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index 97a74aaa014c..ad12547a1d5f 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1052,6 +1052,13 @@ type Service interface { assumeEnabled *bool, ) error + /////////////////////////////////////////////////////////////////////////////// + // Linux MDM + + // LinuxHostDiskEncryptionStatus returns the current disk encryption status of the specified Linux host + // Returns empty status if the host is not a supported Linux host + LinuxHostDiskEncryptionStatus(ctx context.Context, host Host) (HostMDMDiskEncryption, error) + /////////////////////////////////////////////////////////////////////////////// // Common MDM diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 23b95fb5990c..0490ed54740e 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -987,6 +987,8 @@ type ResendHostMDMProfileFunc func(ctx context.Context, hostUUID string, profile type GetHostMDMProfileInstallStatusFunc func(ctx context.Context, hostUUID string, profileUUID string) (fleet.MDMDeliveryStatus, error) +type GetLinuxDiskEncryptionSummaryFunc func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) + type GetMDMCommandPlatformFunc func(ctx context.Context, commandUUID string) (string, error) type ListMDMCommandsFunc func(ctx context.Context, tmFilter fleet.TeamFilter, listOpts *fleet.MDMCommandListOptions) ([]*fleet.MDMCommand, error) @@ -2611,6 +2613,9 @@ type DataStore struct { GetHostMDMProfileInstallStatusFunc GetHostMDMProfileInstallStatusFunc GetHostMDMProfileInstallStatusFuncInvoked bool + GetLinuxDiskEncryptionSummaryFunc GetLinuxDiskEncryptionSummaryFunc + GetLinuxDiskEncryptionSummaryFuncInvoked bool + GetMDMCommandPlatformFunc GetMDMCommandPlatformFunc GetMDMCommandPlatformFuncInvoked bool @@ -6256,6 +6261,13 @@ func (s *DataStore) GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID return s.GetHostMDMProfileInstallStatusFunc(ctx, hostUUID, profileUUID) } +func (s *DataStore) GetLinuxDiskEncryptionSummary(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) { + s.mu.Lock() + s.GetLinuxDiskEncryptionSummaryFuncInvoked = true + s.mu.Unlock() + return s.GetLinuxDiskEncryptionSummaryFunc(ctx, teamID) +} + func (s *DataStore) GetMDMCommandPlatform(ctx context.Context, commandUUID string) (string, error) { s.mu.Lock() s.GetMDMCommandPlatformFuncInvoked = true diff --git a/server/service/hosts.go b/server/service/hosts.go index b2ee905999f7..4af625c2029f 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -1242,6 +1242,20 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f } host.MDM.Profiles = &profiles + if host.IsLUKSSupported() { + status, err := svc.LinuxHostDiskEncryptionStatus(ctx, *host) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get host disk encryption status") + } + host.MDM.OSSettings = &fleet.HostMDMOSSettings{ + DiskEncryption: status, + } + + if status.Status != nil && *status.Status == fleet.DiskEncryptionVerified { + host.MDM.EncryptionKeyAvailable = true + } + } + var macOSSetup *fleet.HostMDMMacOSSetup if ac.MDM.EnabledAndConfigured && license.IsPremium(ctx) { macOSSetup, err = svc.ds.GetHostMDMMacOSSetup(ctx, host.ID) diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index ebca688f3ff6..035a55248672 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -400,6 +400,10 @@ func TestHostDetailsOSSettings(t *testing.T) { return &fleet.HostLockWipeStatus{}, nil } + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{}, nil + } + type testCase struct { name string host *fleet.Host @@ -1316,7 +1320,8 @@ func TestHostEncryptionKey(t *testing.T) { } ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM}, fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, @@ -1369,7 +1374,8 @@ func TestHostEncryptionKey(t *testing.T) { return nil, keyErr } ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM}, fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, @@ -1430,7 +1436,8 @@ func TestHostEncryptionKey(t *testing.T) { return nil } ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return map[fleet.MDMAssetName]fleet.MDMConfigAsset{ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM}, fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM}, diff --git a/server/service/linux_mdm.go b/server/service/linux_mdm.go new file mode 100644 index 000000000000..d4ae8da27e2a --- /dev/null +++ b/server/service/linux_mdm.go @@ -0,0 +1,44 @@ +package service + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +func (svc *Service) LinuxHostDiskEncryptionStatus(ctx context.Context, host fleet.Host) (fleet.HostMDMDiskEncryption, error) { + if !host.IsLUKSSupported() { + return fleet.HostMDMDiskEncryption{}, nil + } + + actionRequired := fleet.DiskEncryptionActionRequired + verified := fleet.DiskEncryptionVerified + failed := fleet.DiskEncryptionFailed + + key, err := svc.ds.GetHostDiskEncryptionKey(ctx, host.ID) + if err != nil { + if fleet.IsNotFound(err) { + return fleet.HostMDMDiskEncryption{ + Status: &actionRequired, + }, nil + } + return fleet.HostMDMDiskEncryption{}, err + } + + if key.ClientError != "" { + return fleet.HostMDMDiskEncryption{ + Status: &failed, + Detail: key.ClientError, + }, nil + } + + if key.Base64Encrypted == "" { + return fleet.HostMDMDiskEncryption{ + Status: &actionRequired, + }, nil + } + + return fleet.HostMDMDiskEncryption{ + Status: &verified, + }, nil +} diff --git a/server/service/linux_mdm_test.go b/server/service/linux_mdm_test.go new file mode 100644 index 000000000000..05809eb4fc96 --- /dev/null +++ b/server/service/linux_mdm_test.go @@ -0,0 +1,118 @@ +package service + +import ( + "context" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mock" + "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/stretchr/testify/assert" +) + +func TestLinuxHostDiskEncryptionStatus(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil) + + actionRequired := fleet.DiskEncryptionActionRequired + verified := fleet.DiskEncryptionVerified + failed := fleet.DiskEncryptionFailed + + testcases := []struct { + name string + host fleet.Host + keyExists bool + clientErrorExists bool + status fleet.HostMDMDiskEncryption + notFound bool + }{ + { + name: "no key", + host: fleet.Host{ID: 1, Platform: "ubuntu"}, + keyExists: false, + clientErrorExists: false, + status: fleet.HostMDMDiskEncryption{ + Status: &actionRequired, + }, + }, + { + name: "key exists", + host: fleet.Host{ID: 1, Platform: "ubuntu"}, + keyExists: true, + clientErrorExists: false, + status: fleet.HostMDMDiskEncryption{ + Status: &verified, + }, + }, + { + name: "key exists && client error", + host: fleet.Host{ID: 1, Platform: "ubuntu"}, + keyExists: true, + clientErrorExists: true, + status: fleet.HostMDMDiskEncryption{ + Status: &failed, + Detail: "client error", + }, + }, + { + name: "no key && client error", + host: fleet.Host{ID: 1, Platform: "ubuntu"}, + keyExists: false, + clientErrorExists: true, + status: fleet.HostMDMDiskEncryption{ + Status: &failed, + Detail: "client error", + }, + }, + { + name: "key not found", + host: fleet.Host{ID: 1, Platform: "ubuntu"}, + keyExists: false, + clientErrorExists: false, + status: fleet.HostMDMDiskEncryption{ + Status: &actionRequired, + }, + notFound: true, + }, + { + name: "unsupported platform", + host: fleet.Host{ID: 1, Platform: "amzn"}, + status: fleet.HostMDMDiskEncryption{}, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) { + var encrypted string + if tt.keyExists { + encrypted = "encrypted" + } + + var clientError string + if tt.clientErrorExists { + clientError = "client error" + } + + var nfe notFoundError + if tt.notFound { + return nil, &nfe + } + + return &fleet.HostDiskEncryptionKey{ + HostID: hostID, + Base64Encrypted: encrypted, + Decryptable: ptr.Bool(true), + UpdatedAt: time.Now(), + ClientError: clientError, + }, nil + } + + status, err := svc.LinuxHostDiskEncryptionStatus(ctx, tt.host) + assert.Nil(t, err) + + assert.Equal(t, tt.status, status) + }) + } +} diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 59ea63c8b4d5..e763cf6c0501 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -606,6 +606,11 @@ func TestMDMCommonAuthorization(t *testing.T) { ds.GetMDMWindowsProfilesSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) { return &fleet.MDMProfilesSummary{}, nil } + + ds.GetLinuxDiskEncryptionSummaryFunc = func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) { + return fleet.MDMLinuxDiskEncryptionSummary{}, nil + } + ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) { res := make(map[string]bool, len(hosts)) for _, h := range hosts { @@ -874,6 +879,11 @@ func TestGetMDMDiskEncryptionSummary(t *testing.T) { return res, nil } + ds.GetLinuxDiskEncryptionSummaryFunc = func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) { + require.Nil(t, teamID) + return fleet.MDMLinuxDiskEncryptionSummary{Verified: 1, ActionRequired: 2, Failed: 3}, nil + } + // Test that the summary properly combines the results of the two methods des, err := svc.GetMDMDiskEncryptionSummary(ctx, nil) require.NoError(t, err) @@ -882,6 +892,7 @@ func TestGetMDMDiskEncryptionSummary(t *testing.T) { Verified: fleet.MDMPlatformsCounts{ MacOS: 1, Windows: 7, + Linux: 1, }, Verifying: fleet.MDMPlatformsCounts{ MacOS: 2, @@ -890,10 +901,12 @@ func TestGetMDMDiskEncryptionSummary(t *testing.T) { ActionRequired: fleet.MDMPlatformsCounts{ MacOS: 3, Windows: 0, + Linux: 2, }, Failed: fleet.MDMPlatformsCounts{ MacOS: 4, Windows: 8, + Linux: 3, }, Enforcing: fleet.MDMPlatformsCounts{ MacOS: 5, diff --git a/server/service/orbit_client.go b/server/service/orbit_client.go index bf355c9cd19d..912d47c86f23 100644 --- a/server/service/orbit_client.go +++ b/server/service/orbit_client.go @@ -22,6 +22,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/constant" "github.com/fleetdm/fleet/v4/orbit/pkg/logging" + "github.com/fleetdm/fleet/v4/orbit/pkg/luks" "github.com/fleetdm/fleet/v4/orbit/pkg/platform" "github.com/fleetdm/fleet/v4/pkg/retry" "github.com/fleetdm/fleet/v4/server/fleet" @@ -668,3 +669,18 @@ func (oc *OrbitClient) GetSetupExperienceStatus() (*fleet.SetupExperienceStatusP return resp.Results, nil } + +func (oc *OrbitClient) SendLinuxKeyEscrowResponse(lr luks.LuksResponse) error { + verb, path := "POST", "/api/fleet/orbit/luks_data" + var resp orbitPostLUKSResponse + if err := oc.authenticatedRequest(verb, path, &orbitPostLUKSRequest{ + Passphrase: lr.Passphrase, + KeySlot: lr.KeySlot, + Salt: lr.Salt, + ClientError: lr.Err, + }, &resp); err != nil { + return err + } + + return nil +} diff --git a/server/test/new_objects.go b/server/test/new_objects.go index 8a285783e5e8..f56496faea86 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -217,6 +217,12 @@ func WithPlatform(s string) NewHostOption { } } +func WithOSVersion(s string) NewHostOption { + return func(h *fleet.Host) { + h.OSVersion = s + } +} + func WithTeamID(teamID uint) NewHostOption { return func(h *fleet.Host) { h.TeamID = &teamID diff --git a/tools/dialog/main.go b/tools/dialog/main.go new file mode 100644 index 000000000000..23e46da66c30 --- /dev/null +++ b/tools/dialog/main.go @@ -0,0 +1,55 @@ +package main + +// This is a tool to test the zenity package on Linux +// It will show an entry dialog, a progress dialog, and an info dialog + +import ( + "context" + "fmt" + "time" + + "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" + "github.com/fleetdm/fleet/v4/orbit/pkg/zenity" +) + +func main() { + prompt := zenity.New() + ctx := context.Background() + + output, err := prompt.ShowEntry(ctx, dialog.EntryOptions{ + Title: "Zenity Test Entry Title", + Text: "Zenity Test Entry Text", + HideText: true, + TimeOut: 10 * time.Second, + }) + if err != nil { + fmt.Println("Err ShowEntry") + panic(err) + } + + ctx, cancelProgress := context.WithCancel(context.Background()) + + go func() { + err := prompt.ShowProgress(ctx, dialog.ProgressOptions{ + Title: "Zenity Test Progress Title", + Text: "Zenity Test Progress Text", + }) + if err != nil { + fmt.Println("Err ShowProgress") + panic(err) + } + }() + + time.Sleep(2 * time.Second) + cancelProgress() + + err = prompt.ShowInfo(ctx, dialog.InfoOptions{ + Title: "Zenity Test Info Title", + Text: "Result: " + string(output), + TimeOut: 10 * time.Second, + }) + if err != nil { + fmt.Println("Err ShowInfo") + panic(err) + } +} diff --git a/tools/luks/luks/main.go b/tools/luks/luks/main.go new file mode 100644 index 000000000000..f20c28f4e289 --- /dev/null +++ b/tools/luks/luks/main.go @@ -0,0 +1,72 @@ +//go:build linux + +package main + +import ( + "context" + "errors" + "fmt" + + "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" + "github.com/fleetdm/fleet/v4/orbit/pkg/lvm" + "github.com/fleetdm/fleet/v4/orbit/pkg/zenity" + "github.com/siderolabs/go-blockdevice/v2/encryption" + "github.com/siderolabs/go-blockdevice/v2/encryption/luks" +) + +func main() { + devicePath, err := lvm.FindRootDisk() + if err != nil { + fmt.Println("devicepath err:", err) + panic(err) + } + + prompt := zenity.New() + + // Prompt existing passphrase from the user. + currentPassphrase, err := prompt.ShowEntry(context.Background(), dialog.EntryOptions{ + Title: "Enter Existing LUKS Passphrase", + Text: "Enter your existing LUKS passphrase:", + HideText: true, + }) + if err != nil { + fmt.Println("Err ShowEntry") + panic(err) + } + + const escrowPassPhrase = "fleet123" + + device := luks.New(luks.AESXTSPlain64Cipher) + + keySlot := 1 + for { + if keySlot == 8 { + panic(errors.New("all LUKS key slots are full")) + } + + userKey := encryption.NewKey(0, currentPassphrase) + escrowKey := encryption.NewKey(keySlot, []byte(escrowPassPhrase)) + + if err := device.AddKey(context.Background(), devicePath, userKey, escrowKey); err != nil { + if errors.Is(err, encryption.ErrEncryptionKeyRejected) { + currentPassphrase, err = prompt.ShowEntry(context.Background(), dialog.EntryOptions{ + Title: "Enter Existing LUKS Passphrase", + Text: "Bad password. Enter your existing LUKS passphrase:", + HideText: true, + }) + if err != nil { + fmt.Println("Err Retry ShowEntry") + panic(err) + } + continue + } + + keySlot++ + continue + } + + break + } + + fmt.Println("Key escrowed successfully.") +} diff --git a/tools/luks/lvm/main.go b/tools/luks/lvm/main.go new file mode 100644 index 000000000000..de6cbe143195 --- /dev/null +++ b/tools/luks/lvm/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + + "github.com/fleetdm/fleet/v4/orbit/pkg/lvm" +) + +func main() { + disk, err := lvm.FindRootDisk() + if err != nil { + panic(err) + } + fmt.Println("Root Partition:", disk) +} From 610a0dd30286044f486804c4db42c577b3aacb58 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:41:19 -0800 Subject: [PATCH 19/36] =?UTF-8?q?to=20RC:=20UI=20-=20fix=20a=20small=20iss?= =?UTF-8?q?ue=20in=20the=20device=20user=20page=20banner=20logic,=20add=20?= =?UTF-8?q?test=E2=80=A6=20(#24020)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### This PR already merged to `main`, see https://github.com/fleetdm/fleet/pull/24001. This is against the release branch so it can be included in 4.60.0. --------- Co-authored-by: Jacob Shandling --- .../DeviceUserBanners.tests.tsx | 25 ++++++++++++++++++- .../DeviceUserBanners/DeviceUserBanners.tsx | 11 +++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx index 632d9eecaf59..0341ad8f00d2 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx @@ -86,7 +86,30 @@ describe("Device User Banners", () => { ).toBeInTheDocument(); }); - it("renders no banner correctly", () => { + it("renders no banner correctly for a mac that is verifying its disk encryption", () => { + render( + + ); + + expect(screen.queryByText(turnOnMdmExpcetedText)).not.toBeInTheDocument(); + expect( + screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText) + ).not.toBeInTheDocument(); + }); + it("renders no banner correctly for a mac without MDM set up", () => { // setup so mdm is not enabled and configured. render( Date: Thu, 21 Nov 2024 13:13:55 -0600 Subject: [PATCH 20/36] Cherry-Pick: Fix Orbit version check in LUKS escrow trigger endpoint (#24027) Cherry-pick of #24026 into the 4.60.0 RC. --- ee/server/service/devices.go | 8 +++++++- server/service/devices_test.go | 26 ++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 9fa82d0e556c..996a4ac74be4 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -208,7 +208,13 @@ func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please enable disk encryption for this host."} } - if host.OrbitVersion == nil || !fleet.IsAtLeastVersion(*host.OrbitVersion, fleet.MinOrbitLUKSVersion) { + // We have to pull Orbit info because the auth context doesn't fill in host.OrbitVersion + orbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID) + if err != nil { + return err + } + + if orbitInfo == nil || !fleet.IsAtLeastVersion(orbitInfo.Version, fleet.MinOrbitLUKSVersion) { return &fleet.BadRequestError{Message: "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version."} } diff --git a/server/service/devices_test.go b/server/service/devices_test.go index 774a941ca06f..4faae0e48019 100644 --- a/server/service/devices_test.go +++ b/server/service/devices_test.go @@ -514,7 +514,7 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { // invalid platform err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "Host platform does not support key escrow") + require.ErrorContains(t, err, "Host platform does not support key escrow") require.True(t, ds.IsHostPendingEscrowFuncInvoked) // valid platform, no-team, encryption not enabled @@ -524,7 +524,7 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { return appConfig, nil } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "Disk encryption is not enabled for hosts not assigned to a team") + require.ErrorContains(t, err, "Disk encryption is not enabled for hosts not assigned to a team") // valid platform, team, encryption not enabled host.TeamID = ptr.Uint(1) @@ -534,29 +534,32 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { return teamConfig, nil } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "Disk encryption is not enabled for this host's team") + require.ErrorContains(t, err, "Disk encryption is not enabled for this host's team") // valid platform, team, host disk is not encrypted or unknown encryption state teamConfig = &fleet.TeamMDM{EnableDiskEncryption: true} err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + require.ErrorContains(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") host.DiskEncryptionEnabled = ptr.Bool(false) err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + require.ErrorContains(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") - // Orbit version is too old + // No Fleet Desktop host.DiskEncryptionEnabled = ptr.Bool(true) - host.OrbitVersion = ptr.String("1.35.1") + orbitInfo := &fleet.HostOrbitInfo{Version: "1.35.1"} + ds.GetHostOrbitInfoFunc = func(ctx context.Context, id uint) (*fleet.HostOrbitInfo, error) { + return orbitInfo, nil + } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version.") + require.ErrorContains(t, err, "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version.") // Encryption key is already escrowed - host.OrbitVersion = ptr.String(fleet.MinOrbitLUKSVersion) + orbitInfo.Version = fleet.MinOrbitLUKSVersion ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error { return errors.New("encryption key is already escrowed") } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.Error(t, err, "encryption key is already escrowed") + require.ErrorContains(t, err, "encryption key is already escrowed") require.Len(t, reportedErrors, 7) }) @@ -570,6 +573,9 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}}, nil } + ds.GetHostOrbitInfoFunc = func(ctx context.Context, id uint) (*fleet.HostOrbitInfo, error) { + return &fleet.HostOrbitInfo{Version: "1.36.0", DesktopVersion: ptr.String("42")}, nil + } ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error { return nil } From 8f94247b973848290387420c3bb8bd95943de4db Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Thu, 21 Nov 2024 13:40:55 -0600 Subject: [PATCH 21/36] Cherry-Pick: Improve LUKS escrow trigger error messages (#24031) Cherry-pick of #24030 into v4.60.0 RC --- ee/server/service/devices.go | 10 +++++----- server/service/devices_test.go | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 996a4ac74be4..77a6c7ce46fe 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -182,7 +182,7 @@ func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host * func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet.Host) error { if !host.IsLUKSSupported() { - return &fleet.BadRequestError{Message: "Host platform does not support key escrow"} + return &fleet.BadRequestError{Message: "Fleet does not yet support creating LUKS disk encryption keys on this platform."} } ac, err := svc.ds.AppConfig(ctx) @@ -192,7 +192,7 @@ func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet if host.TeamID == nil { if !ac.MDM.EnableDiskEncryption.Value { - return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team"} + return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team."} } } else { tc, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID) @@ -200,12 +200,12 @@ func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet return err } if !tc.EnableDiskEncryption { - return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team"} + return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team."} } } if host.DiskEncryptionEnabled == nil || !*host.DiskEncryptionEnabled { - return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please enable disk encryption for this host."} + return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please encrypt your disk first."} } // We have to pull Orbit info because the auth context doesn't fill in host.OrbitVersion @@ -215,7 +215,7 @@ func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet } if orbitInfo == nil || !fleet.IsAtLeastVersion(orbitInfo.Version, fleet.MinOrbitLUKSVersion) { - return &fleet.BadRequestError{Message: "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version."} + return &fleet.BadRequestError{Message: "Your version of fleetd does not support creating disk encryption keys on Linux. Please upgrade fleetd, then click Refetch, then try again."} } return svc.ds.AssertHasNoEncryptionKeyStored(ctx, host.ID) diff --git a/server/service/devices_test.go b/server/service/devices_test.go index 4faae0e48019..53d9644931ac 100644 --- a/server/service/devices_test.go +++ b/server/service/devices_test.go @@ -514,7 +514,7 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { // invalid platform err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.ErrorContains(t, err, "Host platform does not support key escrow") + require.ErrorContains(t, err, "Fleet does not yet support creating LUKS disk encryption keys on this platform.") require.True(t, ds.IsHostPendingEscrowFuncInvoked) // valid platform, no-team, encryption not enabled @@ -524,7 +524,7 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { return appConfig, nil } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.ErrorContains(t, err, "Disk encryption is not enabled for hosts not assigned to a team") + require.ErrorContains(t, err, "Disk encryption is not enabled for hosts not assigned to a team.") // valid platform, team, encryption not enabled host.TeamID = ptr.Uint(1) @@ -534,15 +534,15 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { return teamConfig, nil } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.ErrorContains(t, err, "Disk encryption is not enabled for this host's team") + require.ErrorContains(t, err, "Disk encryption is not enabled for this host's team.") // valid platform, team, host disk is not encrypted or unknown encryption state teamConfig = &fleet.TeamMDM{EnableDiskEncryption: true} err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.ErrorContains(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + require.ErrorContains(t, err, "Host's disk is not encrypted. Please encrypt your disk first.") host.DiskEncryptionEnabled = ptr.Bool(false) err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.ErrorContains(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + require.ErrorContains(t, err, "Host's disk is not encrypted. Please encrypt your disk first.") // No Fleet Desktop host.DiskEncryptionEnabled = ptr.Bool(true) @@ -551,7 +551,7 @@ func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { return orbitInfo, nil } err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) - require.ErrorContains(t, err, "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version.") + require.ErrorContains(t, err, "Your version of fleetd does not support creating disk encryption keys on Linux. Please upgrade fleetd, then click Refetch, then try again.") // Encryption key is already escrowed orbitInfo.Version = fleet.MinOrbitLUKSVersion From e43f34ae9a9f3fe9083eab335739b204198ff3c2 Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:43:09 -0600 Subject: [PATCH 22/36] Apply minimum OS version enforcement to MDM SSO endpoint (#23856) (#24077) --- changes/22361-os-update-ade-sso | 2 + cmd/fleet/serve.go | 3 + server/service/apple_mdm.go | 8 +- server/service/apple_mdm_test.go | 2 +- server/service/handler.go | 42 ++ server/service/integration_mdm_dep_test.go | 464 +++++++++++++++++++++ server/service/integration_mdm_test.go | 1 + server/service/testing_utils.go | 9 + 8 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 changes/22361-os-update-ade-sso diff --git a/changes/22361-os-update-ade-sso b/changes/22361-os-update-ade-sso new file mode 100644 index 000000000000..40221866fb93 --- /dev/null +++ b/changes/22361-os-update-ade-sso @@ -0,0 +1,2 @@ +- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM + IdP integration was enabled. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 4f93d5a540cc..2f7bf65f68c8 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1028,6 +1028,9 @@ the way that the Fleet server works. "get_frontend", service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger), ) + + frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler) + apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore) setupRequired, err := svc.SetupRequired(baseCtx) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 0e0255a09799..da6d28be49bc 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1526,7 +1526,13 @@ func (svc *Service) needsOSUpdateForDEPEnrollment(ctx context.Context, m fleet.M return false, nil } - return apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value) + needsUpdate, err := apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value) + if err != nil { + level.Info(svc.logger).Log("msg", "checking os updates settings, cannot compare versions", "serial", m.Serial, "current_version", m.OSVersion, "minimum_version", settings.MinimumVersion.Value) + return false, nil + } + + return needsUpdate, nil } func (svc *Service) getAppleSoftwareUpdateRequiredForDEPEnrollment(m fleet.MDMAppleMachineInfo) (*fleet.MDMAppleSoftwareUpdateRequired, error) { diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index fc3bc4af70f3..9f07c02629d6 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -4093,7 +4093,7 @@ func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) { SoftwareUpdateDeviceID: "J516sAP", }, updateRequired: nil, - err: "invalid current version", + err: "", // no error, allow enrollment to proceed without software update }, } diff --git a/server/service/handler.go b/server/service/handler.go index d433a7b1124b..4dc5a467b812 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -1231,3 +1232,44 @@ func registerMDM( mux.Handle(apple_mdm.MDMPath, mdmHandler) return nil } + +func WithMDMEnrollmentMiddleware(svc fleet.Service, logger kitlog.Logger, next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/mdm/sso" { + next.ServeHTTP(w, r) + return + } + + // if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os version + di := r.Header.Get("x-apple-aspen-deviceinfo") + if di != "" { + parsed, err := apple_mdm.ParseDeviceinfo(di, false) // FIXME: use verify=true when we have better parsing for various Apple certs (https://github.com/fleetdm/fleet/issues/20879) + if err != nil { + // just log the error and continue to next + level.Error(logger).Log("msg", "parsing x-apple-aspen-deviceinfo", "err", err) + next.ServeHTTP(w, r) + return + } + + sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(r.Context(), parsed) + if err != nil { + // just log the error and continue to next + level.Error(logger).Log("msg", "checking minimum os version for mdm", "err", err) + next.ServeHTTP(w, r) + return + } + + if sur != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + if err := json.NewEncoder(w).Encode(sur); err != nil { + level.Error(logger).Log("msg", "failed to encode software update required", "err", err) + http.Redirect(w, r, r.URL.String()+"?error=true", http.StatusSeeOther) + } + return + } + } + + next.ServeHTTP(w, r) + } +} diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index d2a32c8e33cf..83e8a4b82814 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -1,7 +1,13 @@ package service import ( + "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/base64" "encoding/json" "fmt" "io" @@ -15,13 +21,16 @@ import ( "time" "github.com/fleetdm/fleet/v4/pkg/fleetdbase" + "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" + apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" + "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/worker" kitlog "github.com/go-kit/log" @@ -31,6 +40,7 @@ import ( micromdm "github.com/micromdm/micromdm/mdm/mdm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mozilla.org/pkcs7" ) type profileAssignmentReq struct { @@ -2562,3 +2572,457 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM( // make sure the host gets post enrollment requests checkPostEnrollmentCommands(mdmDevice, true) } + +func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() { + t := s.T() + s.enableABM(t.Name()) + + latestMacOSVersion := "14.6.1" // this is the latest version in our test data (see ../mdm/apple/gdmf/testdata/gdmf.json) + latestMacOSBuild := "23G93" // this is the latest version in our test data (see ../mdm/apple/gdmf/testdata/gdmf.json) + deadline := "2023-12-31" + scepChallenge := "scepcha/> 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) + } + apiHandler := MakeHandler(svc, cfg, logger, limitStore, WithLoginRateLimit(throttled.PerMin(1000))) rootMux.Handle("/api/", apiHandler) var errHandler *errorstore.Handler From 68019f2bb2f1032d97f68fb2a9486135490b7452 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Fri, 22 Nov 2024 14:10:56 -0500 Subject: [PATCH 23/36] feat: do not run setup experience on hosts in a team with no software or script configured (#24073) (#24094) Cherry pick for https://github.com/fleetdm/fleet/pull/24073 --- changes/24024-no-setup-exp | 2 + server/datastore/mysql/setup_experience.go | 9 +- .../datastore/mysql/setup_experience_test.go | 96 +++++++++---------- server/service/integration_mdm_dep_test.go | 14 +-- server/service/orbit.go | 20 +++- 5 files changed, 71 insertions(+), 70 deletions(-) create mode 100644 changes/24024-no-setup-exp diff --git a/changes/24024-no-setup-exp b/changes/24024-no-setup-exp new file mode 100644 index 000000000000..44ab42bcf059 --- /dev/null +++ b/changes/24024-no-setup-exp @@ -0,0 +1,2 @@ +- Modifies the Fleet setup experience feature to not run if there is no software or script + configured for the setup experience. \ No newline at end of file diff --git a/server/datastore/mysql/setup_experience.go b/server/datastore/mysql/setup_experience.go index 328cbb12aec0..33ffa264f087 100644 --- a/server/datastore/mysql/setup_experience.go +++ b/server/datastore/mysql/setup_experience.go @@ -109,8 +109,11 @@ WHERE global_or_team_id = ?` } totalInsertions += uint(inserts) // nolint: gosec - if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil { - return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true") + // Only run setup experience on hosts that have something configured. + if totalInsertions > 0 { + if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil { + return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true") + } } return nil @@ -503,7 +506,7 @@ WHERE host_uuid = ? if err := sqlx.GetContext(ctx, ds.reader(ctx), &awaitingConfiguration, stmt, hostUUID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return false, nil + return false, notFound("HostAwaitingConfiguration") } return false, ctxerr.Wrap(ctx, err, "getting host awaiting configuration") diff --git a/server/datastore/mysql/setup_experience_test.go b/server/datastore/mysql/setup_experience_test.go index fa48a4107c96..1cbece2af264 100644 --- a/server/datastore/mysql/setup_experience_test.go +++ b/server/datastore/mysql/setup_experience_test.go @@ -39,20 +39,22 @@ func TestSetupExperience(t *testing.T) { } } +// TODO(JVE): this test could probably be simplified and most of the ad-hoc SQL removed. func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { ctx := context.Background() test.CreateInsertGlobalVPPToken(t, ds) + // Create some teams team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) require.NoError(t, err) team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) require.NoError(t, err) - team3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team3"}) require.NoError(t, err) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) + // Create some software installers and add them to setup experience tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) require.NoError(t, err) installerID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ @@ -96,6 +98,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { return err }) + // Create some VPP apps and add them to setup experience app1 := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"} vpp1, err := ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID) require.NoError(t, err) @@ -109,33 +112,17 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { return err }) - var script1ID, script2ID int64 - ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { - res, err := insertScriptContents(ctx, q, "SCRIPT 1") - if err != nil { - return err - } - id1, _ := res.LastInsertId() - res, err = insertScriptContents(ctx, q, "SCRIPT 2") - if err != nil { - return err - } - id2, _ := res.LastInsertId() - - res, err = q.ExecContext(ctx, "INSERT INTO setup_experience_scripts (team_id, global_or_team_id, name, script_content_id) VALUES (?, ?, ?, ?)", team1.ID, team1.ID, "script1", id1) - if err != nil { - return err - } - script1ID, _ = res.LastInsertId() + // Create some scripts and add them to setup experience + err = ds.SetSetupExperienceScript(ctx, &fleet.Script{Name: "script1", ScriptContents: "SCRIPT 1", TeamID: &team1.ID}) + require.NoError(t, err) + err = ds.SetSetupExperienceScript(ctx, &fleet.Script{Name: "script2", ScriptContents: "SCRIPT 2", TeamID: &team2.ID}) + require.NoError(t, err) - res, err = q.ExecContext(ctx, "INSERT INTO setup_experience_scripts (team_id, global_or_team_id, name, script_content_id) VALUES (?, ?, ?, ?)", team2.ID, team2.ID, "script2", id2) - if err != nil { - return err - } - script2ID, _ = res.LastInsertId() + script1, err := ds.GetSetupExperienceScript(ctx, &team1.ID) + require.NoError(t, err) - return nil - }) + script2, err := ds.GetSetupExperienceScript(ctx, &team2.ID) + require.NoError(t, err) hostTeam1 := "123" hostTeam2 := "456" @@ -144,14 +131,26 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { anythingEnqueued, err := ds.EnqueueSetupExperienceItems(ctx, hostTeam1, team1.ID) require.NoError(t, err) require.True(t, anythingEnqueued) + awaitingConfig, err := ds.GetHostAwaitingConfiguration(ctx, hostTeam1) + require.NoError(t, err) + require.True(t, awaitingConfig) anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam2, team2.ID) require.NoError(t, err) require.True(t, anythingEnqueued) + awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam2) + require.NoError(t, err) + require.True(t, awaitingConfig) anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam3, team3.ID) require.NoError(t, err) require.False(t, anythingEnqueued) + // Nothing is configured for setup experience in team 3, so we do not set + // host_mdm_apple_awaiting_configuration. + awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam3) + require.Error(t, err) + require.True(t, fleet.IsNotFound(err)) + require.False(t, awaitingConfig) seRows := []setupExperienceInsertTestRows{} @@ -190,13 +189,13 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { HostUUID: hostTeam1, Name: "script1", Status: "pending", - ScriptID: nullableUint(uint(script1ID)), // nolint: gosec + ScriptID: nullableUint(script1.ID), }, { HostUUID: hostTeam2, Name: "script2", Status: "pending", - ScriptID: nullableUint(uint(script2ID)), // nolint: gosec + ScriptID: nullableUint(script2.ID), }, } { var found bool @@ -211,35 +210,28 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { } } - for _, row := range seRows { - if row.HostUUID == hostTeam3 { - t.Error("team 3 shouldn't have any any entries") - } - } - - ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, "DELETE FROM setup_experience_scripts WHERE global_or_team_id = ?", team2.ID) - if err != nil { - return err + require.Condition(t, func() (success bool) { + for _, row := range seRows { + if row.HostUUID == hostTeam3 { + return false + } } - _, err = q.ExecContext(ctx, "UPDATE software_installers SET install_during_setup = false WHERE global_or_team_id = ?", team2.ID) - if err != nil { - return err - } + return true + }) - _, err = q.ExecContext(ctx, "UPDATE vpp_apps_teams SET install_during_setup = false WHERE global_or_team_id = ?", team2.ID) - if err != nil { - return err - } + // Remove team2's setup experience items + err = ds.DeleteSetupExperienceScript(ctx, &team2.ID) + require.NoError(t, err) - return nil - }) + err = ds.SetSetupExperienceSoftwareTitles(ctx, team2.ID, []uint{}) + require.NoError(t, err) anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam1, team1.ID) require.NoError(t, err) require.True(t, anythingEnqueued) + // team2 now has nothing enqueued anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam2, team2.ID) require.NoError(t, err) require.False(t, anythingEnqueued) @@ -271,7 +263,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) { HostUUID: hostTeam1, Name: "script1", Status: "pending", - ScriptID: nullableUint(uint(script1ID)), // nolint: gosec + ScriptID: nullableUint(script1.ID), }, } { var found bool @@ -908,4 +900,10 @@ func testHostInSetupExperience(t *testing.T, ds *Datastore) { inSetupExperience, err = ds.GetHostAwaitingConfiguration(ctx, "abc") require.NoError(t, err) require.False(t, inSetupExperience) + + // host without a record in the table returns not found + inSetupExperience, err = ds.GetHostAwaitingConfiguration(ctx, "404") + require.Error(t, err) + require.True(t, fleet.IsNotFound(err)) + require.False(t, inSetupExperience) } diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 83e8a4b82814..fbdbc4541ac4 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -121,12 +121,6 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceGlobal() { s.enableABM("fleet_ade_test") - // test manual and automatic release with the new setup experience flow - for _, enableReleaseManually := range []bool{false, true} { - t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { - s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false) - }) - } // test manual and automatic release with the old worker flow for _, enableReleaseManually := range []bool{false, true} { t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { @@ -217,12 +211,6 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() { // enable FileVault s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", json.RawMessage([]byte(fmt.Sprintf(`{"enable_disk_encryption":true,"team_id":%d}`, tm.ID))), http.StatusNoContent) - // test manual and automatic release with the new setup experience flow - for _, enableReleaseManually := range []bool{false, true} { - t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { - s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false) - }) - } // test manual and automatic release with the old worker flow for _, enableReleaseManually := range []bool{false, true} { t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { @@ -538,7 +526,7 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de require.NoError(t, err) require.NoError(t, json.Unmarshal(b, &orbitConfigResp)) // should be notified of the setup experience flow - require.True(t, orbitConfigResp.Notifications.RunSetupExperience) + require.False(t, orbitConfigResp.Notifications.RunSetupExperience) if enableReleaseManually { // get the worker's pending job from the future, there should not be any diff --git a/server/service/orbit.go b/server/service/orbit.go index e68bc657f825..1b3b700b0ec5 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -235,19 +235,29 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro } if isConnectedToFleetMDM { + // If there is no software or script configured for setup experience and this is the + // first time orbit is calling the /config endpoint, then this host + // will not have a row in host_mdm_apple_awaiting_configuration. + // On subsequent calls to /config, the host WILL have a row in + // host_mdm_apple_awaiting_configuration. inSetupAssistant, err := svc.ds.GetHostAwaitingConfiguration(ctx, host.UUID) - if err != nil { + if err != nil && !fleet.IsNotFound(err) { return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking if host is in setup experience") } if inSetupAssistant { notifs.RunSetupExperience = true + } - // check if the client is running an old fleetd version that doesn't support the new - // setup experience flow. + if inSetupAssistant || fleet.IsNotFound(err) { + // If the client is running a fleetd that doesn't support setup experience, or if no + // software/script has been configured for setup experience, then we should fall back to + // the "old way" of releasing the device. We do an additional check for + // !inSetupAssistant to prevent enqueuing a new job every time the /config + // endpoint is hit. mp, ok := capabilities.FromContext(ctx) - if !ok || !mp.Has(fleet.CapabilitySetupExperience) { - level.Debug(svc.logger).Log("msg", "host doesn't support Setup experience, falling back to worker-based device release", "host_uuid", host.UUID) + if !ok || !mp.Has(fleet.CapabilitySetupExperience) || !inSetupAssistant { + level.Debug(svc.logger).Log("msg", "host doesn't support setup experience or no setup experience configured, falling back to worker-based device release", "host_uuid", host.UUID) if err := svc.processReleaseDeviceForOldFleetd(ctx, host); err != nil { return fleet.OrbitConfig{}, err } From e439a8979ea682fe824e8511db2041a656d2c740 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Fri, 22 Nov 2024 15:12:51 -0500 Subject: [PATCH 24/36] linux key escrow progress window (#24099) #24069 cherry pick --- orbit/pkg/execuser/execuser.go | 13 ----- orbit/pkg/execuser/execuser_darwin.go | 7 +-- orbit/pkg/execuser/execuser_linux.go | 38 +++----------- orbit/pkg/execuser/execuser_windows.go | 5 -- orbit/pkg/luks/luks_linux.go | 70 ++++++++++++++++++++------ orbit/pkg/zenity/zenity.go | 33 +++++++++--- orbit/pkg/zenity/zenity_test.go | 21 +++++--- 7 files changed, 103 insertions(+), 84 deletions(-) diff --git a/orbit/pkg/execuser/execuser.go b/orbit/pkg/execuser/execuser.go index 5d4aaa353fef..e598bdc2aaaa 100644 --- a/orbit/pkg/execuser/execuser.go +++ b/orbit/pkg/execuser/execuser.go @@ -2,8 +2,6 @@ // SYSTEM service on Windows) as the current login user. package execuser -import "context" - type eopts struct { env [][2]string args [][2]string @@ -51,14 +49,3 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er } return runWithOutput(path, o) } - -// RunWithWait runs an application as the current login user and waits for it to finish -// or to be canceled by the context. Canceling the context will not return an error. -// It assumes the caller is running with high privileges (root on UNIX). -func RunWithWait(ctx context.Context, path string, opts ...Option) error { - var o eopts - for _, fn := range opts { - fn(&o) - } - return runWithWait(ctx, path, o) -} diff --git a/orbit/pkg/execuser/execuser_darwin.go b/orbit/pkg/execuser/execuser_darwin.go index ca92601ba980..6641b40604f7 100644 --- a/orbit/pkg/execuser/execuser_darwin.go +++ b/orbit/pkg/execuser/execuser_darwin.go @@ -1,7 +1,6 @@ package execuser import ( - "context" "errors" "fmt" "io" @@ -10,6 +9,8 @@ import ( ) // run uses macOS open command to start application as the current login user. +// Note that the child process spawns a new process in user space and thus it is not +// effective to add a context to this function to cancel the child process. func run(path string, opts eopts) (lastLogs string, err error) { info, err := os.Stat(path) if err != nil { @@ -53,7 +54,3 @@ func run(path string, opts eopts) (lastLogs string, err error) { func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) { return nil, 0, errors.New("not implemented") } - -func runWithWait(ctx context.Context, path string, opts eopts) error { - return errors.New("not implemented") -} diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go index 1e9614d01be8..5ce487c23c6a 100644 --- a/orbit/pkg/execuser/execuser_linux.go +++ b/orbit/pkg/execuser/execuser_linux.go @@ -3,7 +3,6 @@ package execuser import ( "bufio" "bytes" - "context" "errors" "fmt" "io" @@ -33,6 +32,12 @@ func run(path string, opts eopts) (lastLogs string, err error) { path, ) + if len(opts.args) > 0 { + for _, arg := range opts.args { + args = append(args, arg[0], arg[1]) + } + } + cmd := exec.Command("sudo", args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -74,37 +79,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er return output, exitCode, nil } -func runWithWait(ctx context.Context, path string, opts eopts) error { - args, err := getUserAndDisplayArgs(path, opts) - if err != nil { - return fmt.Errorf("get args: %w", err) - } - - args = append(args, path) - - if len(opts.args) > 0 { - for _, arg := range opts.args { - args = append(args, arg[0], arg[1]) - } - } - - cmd := exec.CommandContext(ctx, "sudo", args...) - log.Printf("cmd=%s", cmd.String()) - - if err := cmd.Start(); err != nil { - return fmt.Errorf("cmd start %q: %w", path, err) - } - - if err := cmd.Wait(); err != nil { - if errors.Is(ctx.Err(), context.Canceled) { - return nil - } - return fmt.Errorf("cmd wait %q: %w", path, err) - } - - return nil -} - func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) { user, err := getLoginUID() if err != nil { diff --git a/orbit/pkg/execuser/execuser_windows.go b/orbit/pkg/execuser/execuser_windows.go index f3bd58038db8..9cf7e9d33855 100644 --- a/orbit/pkg/execuser/execuser_windows.go +++ b/orbit/pkg/execuser/execuser_windows.go @@ -6,7 +6,6 @@ package execuser // To view what was modified/added, you can use the execuser_windows_diff.sh script. import ( - "context" "errors" "fmt" "os" @@ -122,10 +121,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er return nil, 0, errors.New("not implemented") } -func runWithWait(ctx context.Context, path string, opts eopts) error { - return errors.New("not implemented") -} - // getCurrentUserSessionId will attempt to resolve // the session ID of the user currently active on // the system. diff --git a/orbit/pkg/luks/luks_linux.go b/orbit/pkg/luks/luks_linux.go index 9c32307a500e..c45cb74ebffd 100644 --- a/orbit/pkg/luks/luks_linux.go +++ b/orbit/pkg/luks/luks_linux.go @@ -25,10 +25,10 @@ const ( entryDialogTitle = "Enter disk encryption passphrase" entryDialogText = "Passphrase:" retryEntryDialogText = "Passphrase incorrect. Please try again." - infoFailedTitle = "Encryption key escrow" + infoTitle = "Disk encryption" infoFailedText = "Failed to escrow key. Please try again later." - infoSuccessTitle = "Encryption key escrow" - infoSuccessText = "Key escrowed successfully." + infoSuccessText = "Success! Now, return to your browser window and follow the instructions to verify disk encryption." + timeoutMessage = "Please visit Fleet Desktop > My device and click Create key" maxKeySlots = 8 userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase ) @@ -53,6 +53,11 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { response.Err = err.Error() } + if len(key) == 0 && err == nil { + // dialog was canceled or timed out + return nil + } + response.Passphrase = string(key) response.KeySlot = keyslot @@ -76,7 +81,7 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { } // Show error in dialog - if err := lr.infoPrompt(ctx, infoFailedTitle, infoFailedText); err != nil { + if err := lr.infoPrompt(ctx, infoTitle, infoFailedText); err != nil { log.Info().Err(err).Msg("failed to show failed escrow key dialog") } @@ -84,14 +89,14 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error { } if response.Err != "" { - if err := lr.infoPrompt(ctx, infoFailedTitle, response.Err); err != nil { + if err := lr.infoPrompt(ctx, infoTitle, response.Err); err != nil { log.Info().Err(err).Msg("failed to show response error dialog") } return fmt.Errorf("error getting linux escrow key: %s", response.Err) } // Show success dialog - if err := lr.infoPrompt(ctx, infoSuccessTitle, infoSuccessText); err != nil { + if err := lr.infoPrompt(ctx, infoTitle, infoSuccessText); err != nil { log.Info().Err(err).Msg("failed to show success escrow key dialog") } @@ -108,6 +113,19 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err) } + if len(passphrase) == 0 { + log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") + return nil, nil, nil + } + + err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{ + Title: infoTitle, + Text: "Validating passphrase...", + }) + if err != nil { + log.Error().Err(err).Msg("failed to show progress dialog") + } + // Validate the passphrase for { valid, err := lr.passphraseIsValid(ctx, device, devicePath, passphrase, userKeySlot) @@ -123,11 +141,27 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by if err != nil { return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err) } + + if len(passphrase) == 0 { + log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") + return nil, nil, nil + } + + err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{ + Title: infoTitle, + Text: "Validating passphrase...", + }) + if err != nil { + log.Error().Err(err).Msg("failed to show progress dialog after retry") + } } - if len(passphrase) == 0 { - log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out") - return nil, nil, nil + err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{ + Title: infoTitle, + Text: "Key escrow in progress...", + }) + if err != nil { + log.Error().Err(err).Msg("failed to show progress dialog") } escrowPassphrase, err := generateRandomPassphrase() @@ -216,14 +250,18 @@ func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]by TimeOut: 1 * time.Minute, }) if err != nil { - switch err { - case dialog.ErrCanceled: + switch { + case errors.Is(err, dialog.ErrCanceled): log.Debug().Msg("end user canceled key escrow dialog") return nil, nil - case dialog.ErrTimeout: + case errors.Is(err, dialog.ErrTimeout): log.Debug().Msg("key escrow dialog timed out") + err := lr.infoPrompt(ctx, infoTitle, timeoutMessage) + if err != nil { + log.Info().Err(err).Msg("failed to show timeout dialog") + } return nil, nil - case dialog.ErrUnknown: + case errors.Is(err, dialog.ErrUnknown): return nil, err default: return nil, err @@ -237,11 +275,11 @@ func (lr *LuksRunner) infoPrompt(ctx context.Context, title, text string) error err := lr.notifier.ShowInfo(ctx, dialog.InfoOptions{ Title: title, Text: text, - TimeOut: 30 * time.Second, + TimeOut: 1 * time.Minute, }) if err != nil { - switch err { - case dialog.ErrTimeout: + switch { + case errors.Is(err, dialog.ErrTimeout): log.Debug().Msg("successPrompt timed out") return nil default: diff --git a/orbit/pkg/zenity/zenity.go b/orbit/pkg/zenity/zenity.go index bd51d214f82b..2d2989f9d804 100644 --- a/orbit/pkg/zenity/zenity.go +++ b/orbit/pkg/zenity/zenity.go @@ -7,28 +7,37 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/dialog" "github.com/fleetdm/fleet/v4/orbit/pkg/execuser" + "github.com/fleetdm/fleet/v4/orbit/pkg/platform" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/rs/zerolog/log" ) +const zenityProcessName = "zenity" + type Zenity struct { // cmdWithOutput can be set in tests to mock execution of the dialog. cmdWithOutput func(ctx context.Context, args ...string) ([]byte, int, error) // cmdWithWait can be set in tests to mock execution of the dialog. cmdWithWait func(ctx context.Context, args ...string) error + // killZenityFunc can be set in tests to mock killing the zenity process. + killZenityFunc func() } // New creates a new Zenity dialog instance for zenity v4 on Linux. // Zenity implements the Dialog interface. func New() *Zenity { return &Zenity{ - cmdWithOutput: execCmdWithOutput, - cmdWithWait: execCmdWithWait, + cmdWithOutput: execCmdWithOutput, + cmdWithWait: execCmdWithWait, + killZenityFunc: killZenityProcesses, } } // ShowEntry displays an dialog that accepts end user input. It returns the entered // text or errors ErrCanceled, ErrTimeout, or ErrUnknown. func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byte, error) { + z.killZenityFunc() + args := []string{"--entry"} if opts.Title != "" { args = append(args, fmt.Sprintf("--title=%s", opts.Title)) @@ -47,9 +56,9 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt if err != nil { switch statusCode { case 1: - return nil, ctxerr.Wrap(ctx, dialog.ErrCanceled) + return nil, dialog.ErrCanceled case 5: - return nil, ctxerr.Wrap(ctx, dialog.ErrTimeout) + return nil, dialog.ErrTimeout default: return nil, ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error()) } @@ -60,6 +69,8 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt // ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown. func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error { + z.killZenityFunc() + args := []string{"--info"} if opts.Title != "" { args = append(args, fmt.Sprintf("--title=%s", opts.Title)) @@ -94,6 +105,8 @@ func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error { // Use this function for cases where a progress dialog is needed to run // alongside other operations, with explicit cancellation or termination. func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions) error { + z.killZenityFunc() + args := []string{"--progress"} if opts.Title != "" { args = append(args, fmt.Sprintf("--title=%s", opts.Title)) @@ -122,7 +135,7 @@ func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error) opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args } - output, exitCode, err := execuser.RunWithOutput("zenity", opts...) + output, exitCode, err := execuser.RunWithOutput(zenityProcessName, opts...) // Trim the newline from zenity output output = bytes.TrimSuffix(output, []byte("\n")) @@ -136,5 +149,13 @@ func execCmdWithWait(ctx context.Context, args ...string) error { opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args } - return execuser.RunWithWait(ctx, "zenity", opts...) + _, err := execuser.Run(zenityProcessName, opts...) + return err +} + +func killZenityProcesses() { + _, err := platform.KillAllProcessByName(zenityProcessName) + if err != nil { + log.Warn().Err(err).Msg("failed to kill zenity process") + } } diff --git a/orbit/pkg/zenity/zenity_test.go b/orbit/pkg/zenity/zenity_test.go index 5d57f52d91ff..f7b2337f8cc2 100644 --- a/orbit/pkg/zenity/zenity_test.go +++ b/orbit/pkg/zenity/zenity_test.go @@ -76,7 +76,8 @@ func TestShowEntryArgs(t *testing.T) { output: []byte("some output"), } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } output, err := z.ShowEntry(ctx, tt.opts) assert.NoError(t, err) @@ -117,7 +118,8 @@ func TestShowEntryError(t *testing.T) { exitCode: tt.exitCode, } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } output, err := z.ShowEntry(ctx, dialog.EntryOptions{}) require.ErrorIs(t, err, tt.expectedErr) @@ -133,7 +135,8 @@ func TestShowEntrySuccess(t *testing.T) { output: []byte("some output"), } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } output, err := z.ShowEntry(ctx, dialog.EntryOptions{}) assert.NoError(t, err) @@ -168,7 +171,8 @@ func TestShowInfoArgs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockExecCmd{} z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } err := z.ShowInfo(ctx, tt.opts) assert.NoError(t, err) @@ -203,7 +207,8 @@ func TestShowInfoError(t *testing.T) { exitCode: tt.exitCode, } z := &Zenity{ - cmdWithOutput: mock.runWithOutput, + cmdWithOutput: mock.runWithOutput, + killZenityFunc: func() {}, } err := z.ShowInfo(ctx, dialog.InfoOptions{}) require.ErrorIs(t, err, tt.expectedErr) @@ -233,7 +238,8 @@ func TestProgressArgs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockExecCmd{} z := &Zenity{ - cmdWithWait: mock.runWithWait, + cmdWithWait: mock.runWithWait, + killZenityFunc: func() {}, } err := z.ShowProgress(ctx, tt.opts) assert.NoError(t, err) @@ -249,7 +255,8 @@ func TestProgressKillOnCancel(t *testing.T) { waitDuration: 5 * time.Second, } z := &Zenity{ - cmdWithWait: mock.runWithWait, + cmdWithWait: mock.runWithWait, + killZenityFunc: func() {}, } done := make(chan struct{}) From e18729c52c0be1bba485c7144989ddbc1d01bff4 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Fri, 22 Nov 2024 15:21:13 -0600 Subject: [PATCH 25/36] Fix teams modal only showing two options (#23889) (#24080) --- frontend/components/Modal/_styles.scss | 2 +- frontend/components/forms/fields/Dropdown/_styles.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index e12ba664092f..433e2e886d8c 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -26,7 +26,7 @@ margin-top: $pad-large; font-size: $x-small; max-height: 800px; - overflow-y: auto; + overflow: visible; .input-field { width: 100%; diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss index c86dc61bf62c..dc7a763f7c3e 100644 --- a/frontend/components/forms/fields/Dropdown/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/_styles.scss @@ -225,6 +225,10 @@ animation: fade-in 150ms ease-out; } + .Select-menu { + max-height: 190px; + } + .Select-noresults { font-size: $x-small; } From be15eec9fd6959e0046927cee86f421d2a16c941 Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Mon, 25 Nov 2024 10:01:24 -0600 Subject: [PATCH 26/36] Cherry-Pick: Include Linux disk encryption status in configuration profiles aggregate status response when applicable, fix disk encryption/MDM configuration order-of-operations issues, add integration tests for LUKS (#24124) Cherry-pick of #24114, for #24112, #24116, #23587 Co-authored-by: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Co-authored-by: Jacob Shandling --- ee/server/service/teams.go | 18 +- frontend/services/entities/mdm.ts | 2 +- frontend/utilities/endpoints.ts | 2 +- server/datastore/mysql/app_configs.go | 2 +- server/datastore/mysql/app_configs_test.go | 6 +- server/datastore/mysql/hosts.go | 3 +- server/datastore/mysql/labels.go | 2 +- server/datastore/mysql/microsoft_mdm.go | 6 +- server/fleet/datastore.go | 1 + server/fleet/mdm.go | 4 +- server/fleet/service.go | 5 + server/mock/datastore_mock.go | 12 ++ server/service/appconfig.go | 2 +- server/service/apple_mdm.go | 9 +- server/service/integration_core_test.go | 7 +- server/service/integration_enterprise_test.go | 162 ++++++++++++++++++ server/service/linux_mdm.go | 26 +++ server/service/mdm.go | 59 ++++++- 18 files changed, 293 insertions(+), 35 deletions(-) diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 20a526197bf5..eded9b4788ac 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1516,15 +1516,15 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) { } func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.Team, enable *bool) error { - var didUpdate, didUpdateMacOSDiskEncryption bool + var didUpdate bool if enable != nil { - if svc.config.Server.PrivateKey == "" { - return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } if tm.Config.MDM.EnableDiskEncryption != *enable { + if *enable && svc.config.Server.PrivateKey == "" { + return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + tm.Config.MDM.EnableDiskEncryption = *enable didUpdate = true - didUpdateMacOSDiskEncryption = true } } @@ -1537,13 +1537,7 @@ func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.T if err != nil { return err } - - // macOS-specific stuff. For legacy reasons we check if apple is configured - // via `appCfg.MDM.EnabledAndConfigured` - // - // TODO: is there a missing bitlocker activity feed item? (see same TODO on - // other methods that deal with disk encryption) - if appCfg.MDM.EnabledAndConfigured && didUpdateMacOSDiskEncryption { + if appCfg.MDM.EnabledAndConfigured { var act fleet.ActivityDetails if tm.Config.MDM.EnableDiskEncryption { act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name} diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index ec7499390b40..aab412772ae7 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -168,7 +168,7 @@ const mdmService = { }, getProfilesStatusSummary: (teamId: number) => { - let { MDM_PROFILES_STATUS_SUMMARY: path } = endpoints; + let { PROFILES_STATUS_SUMMARY: path } = endpoints; if (teamId) { path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 196345bf2bbe..a1acd94adee5 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -114,7 +114,7 @@ export default { MDM_PROFILE: (id: string) => `/${API_VERSION}/fleet/mdm/profiles/${id}`, MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`, - MDM_PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/mdm/profiles/summary`, + PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/configuration_profiles/summary`, MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`, MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`, MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => { diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 8f8d708eb908..5de25d4d0006 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -274,7 +274,7 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet return secrets, nil } -func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { +func (ds *Datastore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { if teamID != nil && *teamID > 0 { tc, err := ds.TeamMDMConfig(ctx, *teamID) if err != nil { diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index dc0d4b1c9d9d..46df41010be8 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -449,7 +449,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { require.NoError(t, err) require.False(t, ac.MDM.EnableDiskEncryption.Value) - enabled, err := ds.getConfigEnableDiskEncryption(ctx, nil) + enabled, err := ds.GetConfigEnableDiskEncryption(ctx, nil) require.NoError(t, err) require.False(t, enabled) @@ -461,7 +461,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { require.NoError(t, err) require.True(t, ac.MDM.EnableDiskEncryption.Value) - enabled, err = ds.getConfigEnableDiskEncryption(ctx, nil) + enabled, err = ds.GetConfigEnableDiskEncryption(ctx, nil) require.NoError(t, err) require.True(t, enabled) @@ -474,7 +474,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { require.NotNil(t, tm) require.False(t, tm.Config.MDM.EnableDiskEncryption) - enabled, err = ds.getConfigEnableDiskEncryption(ctx, &team1.ID) + enabled, err = ds.GetConfigEnableDiskEncryption(ctx, &team1.ID) require.NoError(t, err) require.False(t, enabled) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index b2f7f3a650ed..0de58ba918a0 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1218,7 +1218,7 @@ func (ds *Datastore) applyHostFilters( return "", nil, ctxerr.Wrap(ctx, err, "building query to filter macOS settings status") } sqlStmt, whereParams = filterHostsByMacOSDiskEncryptionStatus(sqlStmt, opt, whereParams) - if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { + if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { if errors.Is(err, sql.ErrNoRows) { return "", nil, ctxerr.Wrap( ctx, &fleet.BadRequestError{ @@ -4547,6 +4547,7 @@ func (ds *Datastore) HostLite(ctx context.Context, id uint) (*fleet.Host, error) "hardware_model", "computer_name", "platform", + "os_version", "team_id", "distributed_interval", "logger_tls_period", diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index 21c4e0eb9402..4111c81b6ee0 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -662,7 +662,7 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea } query, whereParams = filterHostsByMacOSDiskEncryptionStatus(query, opt, whereParams) query, whereParams = filterHostsByMDMBootstrapPackageStatus(query, opt, whereParams) - if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { + if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { return "", nil, err } else if opt.OSSettingsFilter.IsValid() { query, whereParams, err = ds.filterHostsByOSSettingsStatus(query, opt, whereParams, enableDiskEncryption) diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 9e773afddb06..e846ef8af79e 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -585,7 +585,7 @@ AND ( } func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) { - enabled, err := ds.getConfigEnableDiskEncryption(ctx, teamID) + enabled, err := ds.GetConfigEnableDiskEncryption(ctx, teamID) if err != nil { return nil, err } @@ -655,7 +655,7 @@ func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fle return nil, nil } - enabled, err := ds.getConfigEnableDiskEncryption(ctx, host.TeamID) + enabled, err := ds.GetConfigEnableDiskEncryption(ctx, host.TeamID) if err != nil { return nil, err } @@ -887,7 +887,7 @@ func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}, e } func (ds *Datastore) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) { - includeBitLocker, err := ds.getConfigEnableDiskEncryption(ctx, teamID) + includeBitLocker, err := ds.GetConfigEnableDiskEncryption(ctx, teamID) if err != nil { return nil, err } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 5979c7ade843..2a536c8a8dde 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -899,6 +899,7 @@ type Datastore interface { GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error + GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for // a host diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 55e28bc7b945..4d3e09f68ec8 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -310,8 +310,8 @@ type MDMDiskEncryptionSummary struct { RemovingEnforcement MDMPlatformsCounts `db:"removing_enforcement" json:"removing_enforcement"` } -// MDMProfilesSummary reports the number of hosts being managed with MDM configuration -// profiles. Each host may be counted in only one of four mutually-exclusive categories: +// MDMProfilesSummary reports the number of hosts being managed with configuration +// profiles and/or disk encryption. Each host may be counted in only one of four mutually-exclusive categories: // Failed, Pending, Verifying, or Verified. type MDMProfilesSummary struct { // Verified includes each host where Fleet has verified the installation of all of the diff --git a/server/fleet/service.go b/server/fleet/service.go index ad12547a1d5f..7e9f7c973cbb 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1059,6 +1059,11 @@ type Service interface { // Returns empty status if the host is not a supported Linux host LinuxHostDiskEncryptionStatus(ctx context.Context, host Host) (HostMDMDiskEncryption, error) + // GetMDMLinuxProfilesSummary summarizes the current status of Linux disk encryption for + // the provided team (or hosts without a team if teamId is nil), or returns zeroes if disk + // encryption is not enforced on the selected team + GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (MDMProfilesSummary, error) + /////////////////////////////////////////////////////////////////////////////// // Common MDM diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 0490ed54740e..7c1f909f0ba6 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -635,6 +635,8 @@ type GetHostEmailsFunc func(ctx context.Context, hostUUID string, source string) type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error +type GetConfigEnableDiskEncryptionFunc func(ctx context.Context, teamID *uint) (bool, error) + type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error @@ -2085,6 +2087,9 @@ type DataStore struct { SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFuncInvoked bool + GetConfigEnableDiskEncryptionFunc GetConfigEnableDiskEncryptionFunc + GetConfigEnableDiskEncryptionFuncInvoked bool + SetOrUpdateHostDisksEncryptionFunc SetOrUpdateHostDisksEncryptionFunc SetOrUpdateHostDisksEncryptionFuncInvoked bool @@ -5029,6 +5034,13 @@ func (s *DataStore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, return s.SetOrUpdateHostDisksSpaceFunc(ctx, hostID, gigsAvailable, percentAvailable, gigsTotal) } +func (s *DataStore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { + s.mu.Lock() + s.GetConfigEnableDiskEncryptionFuncInvoked = true + s.mu.Unlock() + return s.GetConfigEnableDiskEncryptionFunc(ctx, teamID) +} + func (s *DataStore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error { s.mu.Lock() s.SetOrUpdateHostDisksEncryptionFuncInvoked = true diff --git a/server/service/appconfig.go b/server/service/appconfig.go index ad8778b57035..030c2c650202 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -416,7 +416,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle // 1. To get the JSON value from the database // 2. To update fields with the incoming values if newAppConfig.MDM.EnableDiskEncryption.Valid { - if svc.config.Server.PrivateKey == "" { + if newAppConfig.MDM.EnableDiskEncryption.Value && svc.config.Server.PrivateKey == "" { return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } appConfig.MDM.EnableDiskEncryption = newAppConfig.MDM.EnableDiskEncryption diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index da6d28be49bc..6ad19bb1eb97 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2143,12 +2143,15 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable return err } - var didUpdate, didUpdateMacOSDiskEncryption bool + var didUpdate bool if enabled != nil { if ac.MDM.EnableDiskEncryption.Value != *enabled { + if *enabled && svc.config.Server.PrivateKey == "" { + return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + ac.MDM.EnableDiskEncryption = optjson.SetBool(*enabled) didUpdate = true - didUpdateMacOSDiskEncryption = true } } @@ -2156,7 +2159,7 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable if err := svc.ds.SaveAppConfig(ctx, ac); err != nil { return err } - if didUpdateMacOSDiskEncryption { + if ac.MDM.EnabledAndConfigured { // if macOS MDM is configured, set up FileVault escrow var act fleet.ActivityDetails if ac.MDM.EnableDiskEncryption.Value { act = fleet.ActivityTypeEnabledMacosDiskEncryption{} diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 999b6bfb34d6..0dfab1f02f1c 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8638,6 +8638,11 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() { require.Equal(t, hostLin.ID, getHostResp.Host.ID) require.True(t, *getHostResp.Host.DiskEncryptionEnabled) + // should succeed as we no longer require MDM to access this endpoint, as Linux encryption doesn't require MDM + var profiles getMDMProfilesSummaryResponse + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles) + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles) + // set unencrypted for all hosts require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostWin.ID, false)) require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostMac.ID, false)) @@ -8653,7 +8658,7 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() { require.Equal(t, hostMac.ID, getHostResp.Host.ID) require.False(t, *getHostResp.Host.DiskEncryptionEnabled) - // Linux does not return false, it omits the field when false + // Linux may omit the field when false getHostResp = getHostResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostLin.ID), nil, http.StatusOK, &getHostResp) require.Equal(t, hostLin.ID, getHostResp.Host.ID) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 649d0b5912d0..93bac5241052 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -2881,6 +2881,168 @@ func (s *integrationEnterpriseTestSuite) TestAppleOSUpdatesTeamConfig() { }, http.StatusUnprocessableEntity, &tmResp) } +func (s *integrationEnterpriseTestSuite) TestLinuxDiskEncryption() { + t := s.T() + + // create a Linux host + noTeamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"), + OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"), + UUID: t.Name() + "3", + Hostname: t.Name() + "foo3.local", + PrimaryIP: "192.168.1.3", + PrimaryMac: "30-65-EC-6F-C4-60", + Platform: "ubuntu", + OSVersion: "Ubuntu 22.04", + }) + require.NoError(t, err) + orbitKey := setOrbitEnrollment(t, noTeamHost, s.ds) + noTeamHost.OrbitNodeKey = &orbitKey + + team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "A team"}) + require.NoError(t, err) + teamID := ptr.Uint(team.ID) + teamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"), + OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"), + UUID: t.Name() + "2", + Hostname: t.Name() + "foo2.local", + PrimaryIP: "192.168.1.2", + PrimaryMac: "30-65-EC-6F-C4-59", + Platform: "rhel", + OSVersion: "Fedora 38.0", // this check is why HostLite now includes os_version in the data it's selecting + TeamID: teamID, + }) + require.NoError(t, err) + teamOrbitKey := setOrbitEnrollment(t, teamHost, s.ds) + teamHost.OrbitNodeKey = &teamOrbitKey + + // NO TEAM // + + // config profiles endpoint should work but show all zeroes + var profileSummary getMDMProfilesSummaryResponse + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // set encrypted for host + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), noTeamHost.ID, true)) + + // should still show zeroes + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // turn on disk encryption enforcement + s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent) + + // should show the Linux host as pending + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary) + + // encryption summary should succeed (Linux encryption doesn't require MDM) + var summary getMDMDiskEncryptionSummaryResponse + s.DoJSON("GET", "/api/latest/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary) + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary) + // disk is encrypted but key hasn't been escrowed yet + require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // trigger escrow process from device + token := "much_valid" + mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, noTeamHost.ID, token) + return err + }) + // should fail because default Orbit version is too old + res := s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusBadRequest) + res.Body.Close() + + // should succeed now that Orbit version isn't too old + require.NoError(t, s.ds.SetOrUpdateHostOrbitInfo(context.Background(), noTeamHost.ID, fleet.MinOrbitLUKSVersion, sql.NullString{}, sql.NullBool{})) + res = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusNoContent) + res.Body.Close() + + // confirm that Orbit endpoint shows notification flag + var orbitResponse orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &orbitResponse) + require.True(t, orbitResponse.Notifications.RunDiskEncryptionEscrow) + + // confirm that second Orbit pull doesn't show notification flag + var secondOrbitResponse orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &secondOrbitResponse) + require.False(t, secondOrbitResponse.Notifications.RunDiskEncryptionEscrow) + + // set an error first; the successful write should overwrite that + s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{ + OrbitNodeKey: *noTeamHost.OrbitNodeKey, + ClientError: "Houston, we had a problem", + }, http.StatusNoContent) + + // upload LUKS data + keySlot := ptr.Uint(1) + s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{ + OrbitNodeKey: *noTeamHost.OrbitNodeKey, + Passphrase: "whale makes pail rise", + Salt: "the team i like lost", + KeySlot: keySlot, + }, http.StatusNoContent) + + // confirm verified + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary) + require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // get passphrase back + var keyResponse getHostEncryptionKeyResponse + s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/mdm/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse) + s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse) + require.Equal(t, "whale makes pail rise", keyResponse.EncryptionKey.DecryptedValue) + + // TEAM // + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // set encrypted for host + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), teamHost.ID, true)) + + // should still show zeroes + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // turn on disk encryption enforcement for team + s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{TeamID: teamID, EnableDiskEncryption: true}, http.StatusNoContent) + + // should show the Linux host as pending + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary) + + // encryption summary should show host as action required + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary) + require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // upload LUKS data (no error, and no trigger, first this time) + keySlot = ptr.Uint(3) + s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{ + OrbitNodeKey: *teamHost.OrbitNodeKey, + Passphrase: "the mome raths outgrabe", + Salt: "jabberwocky, but salty", + KeySlot: keySlot, + }, http.StatusNoContent) + + // confirm verified + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary) + require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // get passphrase back + s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, teamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse) + require.Equal(t, "the mome raths outgrabe", keyResponse.EncryptionKey.DecryptedValue) +} + func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() { t := s.T() ctx := context.Background() diff --git a/server/service/linux_mdm.go b/server/service/linux_mdm.go index d4ae8da27e2a..815e2bf04d7f 100644 --- a/server/service/linux_mdm.go +++ b/server/service/linux_mdm.go @@ -3,6 +3,7 @@ package service import ( "context" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -42,3 +43,28 @@ func (svc *Service) LinuxHostDiskEncryptionStatus(ctx context.Context, host flee Status: &verified, }, nil } + +func (svc *Service) GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (summary fleet.MDMProfilesSummary, err error) { + if err = svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamId}, fleet.ActionRead); err != nil { + return summary, ctxerr.Wrap(ctx, err) + } + + // Linux doesn't have configuration profiles, so if we aren't enforcing disk encryption we have nothing to report + includeDiskEncryptionStats, err := svc.ds.GetConfigEnableDiskEncryption(ctx, teamId) + if err != nil { + return summary, ctxerr.Wrap(ctx, err) + } else if !includeDiskEncryptionStats { + return summary, nil + } + + counts, err := svc.ds.GetLinuxDiskEncryptionSummary(ctx, teamId) + if err != nil { + return summary, ctxerr.Wrap(ctx, err) + } + + return fleet.MDMProfilesSummary{ + Verified: counts.Verified, + Pending: counts.ActionRequired, + Failed: counts.Failed, + }, nil +} diff --git a/server/service/mdm.go b/server/service/mdm.go index 12876b95ddd7..41fc1f789e40 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -907,7 +907,8 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin } //////////////////////////////////////////////////////////////////////////////// -// GET /mdm/profiles/summary +// GET /mdm/profiles/summary (deprecated) +// GET /configuration_profiles/summary //////////////////////////////////////////////////////////////////////////////// type getMDMProfilesSummaryRequest struct { @@ -935,10 +936,15 @@ func getMDMProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc return &getMDMProfilesSummaryResponse{Err: err}, nil } - res.Verified = as.Verified + ws.Verified + ls, err := svc.GetMDMLinuxProfilesSummary(ctx, req.TeamID) + if err != nil { + return &getMDMProfilesSummaryResponse{Err: err}, nil + } + + res.Verified = as.Verified + ws.Verified + ls.Verified res.Verifying = as.Verifying + ws.Verifying - res.Failed = as.Failed + ws.Failed - res.Pending = as.Pending + ws.Pending + res.Failed = as.Failed + ws.Failed + ls.Failed + res.Pending = as.Pending + ws.Pending + ls.Pending return &res, nil } @@ -2606,9 +2612,52 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek return ctxerr.Wrap(ctx, err, "retrieving app config") } + wasEnabledAndConfigured := appCfg.MDM.EnabledAndConfigured appCfg.MDM.EnabledAndConfigured = true + err = svc.ds.SaveAppConfig(ctx, appCfg) + if err != nil { + return ctxerr.Wrap(ctx, err, "saving app config") + } - return svc.ds.SaveAppConfig(ctx, appCfg) + // Disk encryption can be enabled prior to Apple MDM being configured, but we need MDM to be set up to escrow + // FileVault keys. We handle the other order of operations elsewhere (on encryption enable, after checking to see + // if Mac MDM is already enabled). We skip this step if we were just re-uploading an APNs cert when MDM was already + // enabled. + if wasEnabledAndConfigured { + return nil + } + + // Enable FileVault escrow if no-team already has disk encryption enforced + if appCfg.MDM.EnableDiskEncryption.Value { + if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, nil); err != nil { + return ctxerr.Wrap(ctx, err, "enable no-team FileVault escrow") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{}); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for enabling no-team macOS disk encryption") + } + } + // Enable FileVault escrow for teams that already have disk encryption enforced + // For later: add a data store method to avoid making an extra query per team to check whether encryption is enforced + teams, err := svc.ds.TeamsSummary(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "listing teams") + } + for _, team := range teams { + isEncryptionEnforced, err := svc.ds.GetConfigEnableDiskEncryption(ctx, &team.ID) + if err != nil { + return ctxerr.Wrap(ctx, err, "retrieving encryption enforcement status for team") + } + if isEncryptionEnforced { + if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil { + return ctxerr.Wrap(ctx, err, "enable FileVault escrow for team") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for enabling macOS disk encryption for team") + } + } + } + + return nil } //////////////////////////////////////////////////////////////////////////////// From eff83e47ba8d2083708246754c9590666d696367 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:24:07 -0500 Subject: [PATCH 27/36] For R.C. - Fleet UI: 4.60 unreleased bug fix for scrollable content (#24144) --- frontend/components/Modal/_styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index 433e2e886d8c..ef41106731ab 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -25,7 +25,7 @@ &__content-wrapper { margin-top: $pad-large; font-size: $x-small; - max-height: 800px; + // New pattern of max height modals pushed to 4.61 with PR #24019 overflow: visible; .input-field { From e6b829928468b9540970b0c3a065e2021bdc247d Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:50:42 -0800 Subject: [PATCH 28/36] =?UTF-8?q?to=20RC:=20UI=20=E2=80=93=2011/26=20Disk?= =?UTF-8?q?=20encryption=20spec=20updates=20(#24175)=20(#24178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### This PR already merged to `main`, see https://github.com/fleetdm/fleet/pull/24175. This is against the release branch so it can be included in 4.60.0. Co-authored-by: Jacob Shandling --- .../cards/DiskEncryption/DiskEncryption.tsx | 23 +++++++++- .../HostDetailsBanners/HostDetailsBanners.tsx | 42 ++++++++++++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx index 16e773809d7d..fd4fe0331073 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx @@ -124,7 +124,24 @@ const DiskEncryption = ({ setIsLoadingTeam(false); } - const getTipContent = (platform: "windows" | "macOS") => { + const getTipContent = (platform: "windows" | "macOS" | "linux") => { + if (platform === "linux") { + return ( + <> + For Ubuntu and Fedora Linux. +
+ Currently, full disk encryption must be turned on{" "} + + during OS +
+ setup +
+ . If disk encryption is off, the end user must re-install +
+ their operating system. + + ); + } const [AppleOrWindows, DEMethod] = platform === "windows" ? ["Windows", "BitLocker"] @@ -149,7 +166,9 @@ const DiskEncryption = ({ Windows - , Ubuntu Linux, and Fedora Linux hosts. + , and{" "} + Linux{" "} + hosts. ); diff --git a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx index 188282bab132..0e4eb8b851a4 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx @@ -1,15 +1,21 @@ import React, { useContext } from "react"; import { AppContext } from "context/app"; -import { DiskEncryptionStatus, MdmEnrollmentStatus } from "interfaces/mdm"; import { hasLicenseExpired } from "utilities/helpers"; -import InfoBanner from "components/InfoBanner"; + +import { DiskEncryptionStatus, MdmEnrollmentStatus } from "interfaces/mdm"; import { IOSSettings } from "interfaces/host"; import { HostPlatform, + isDiskEncryptionSupportedLinuxPlatform, platformSupportsDiskEncryption, } from "interfaces/platform"; +import InfoBanner from "components/InfoBanner"; +import CustomLink from "components/CustomLink"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; +import { isDiskEncryptionProfile } from "pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers"; + const baseClass = "host-details-banners"; export interface IHostBannersBaseProps { @@ -110,9 +116,35 @@ const HostDetailsBanners = ({ platformSupportsDiskEncryption(hostPlatform, hostOsVersion) && diskEncryptionOSSetting?.status ) { - // host either not in compliance with setting, or is but Fleet doesn't yet have a disk - // encryption key escrowed for the host (possible for Linux hosts) - if (!diskIsEncrypted || !diskEncryptionKeyAvailable) { + if ( + !diskIsEncrypted && + isDiskEncryptionSupportedLinuxPlatform(hostPlatform, hostOsVersion ?? "") + ) { + // linux host not in compliance with setting + return ( +
+ + } + > + Disk encryption: Disk encryption is off. Currently, to turn on{" "} + full disk encryption, the end user has to re-install their + operating system. + +
+ ); + } + if (!diskEncryptionKeyAvailable) { + // disk is encrypted, but Fleet doesn't yet have a disk + // encryption key escrowed (possible for Linux hosts) return (
From 4a6b5d5b1df9459fe2152e2577aefca18a5d8c19 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:18:41 -0800 Subject: [PATCH 29/36] to RC: UI - Fix DUP banners for Fedora disk encryption (#24153) (#24179) #### This PR already merged to `main`, see https://github.com/fleetdm/fleet/pull/24153. This is against the release branch so it can be included in 4.60.0. --------- Co-authored-by: Jacob Shandling --- .../details/DeviceUserPage/DeviceUserPage.tsx | 1 + .../DeviceUserBanners/DeviceUserBanners.tsx | 5 +- .../cards/HostSummary/HostSummary.tests.tsx | 60 +++++++++---------- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 868383d2379b..f2f44a48a8dd 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -371,6 +371,7 @@ const DeviceUserPage = ({
{ }); }); - it("omit fleet desktop from tooltip if no fleet desktop version", async () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary({ - fleet_desktop_version: null, - }); - const orbitVersion = summaryData.orbit_version as string; - const osqueryVersion = summaryData.osquery_version as string; + // it("omit fleet desktop from tooltip if no fleet desktop version", async () => { + // const render = createCustomRenderer({ + // context: { + // app: { + // isPremiumTier: true, + // isGlobalAdmin: true, + // currentUser: createMockUser(), + // }, + // }, + // }); + // const summaryData = createMockHostSummary({ + // fleet_desktop_version: null, + // }); + // const orbitVersion = summaryData.orbit_version as string; + // const osqueryVersion = summaryData.osquery_version as string; - const { user } = render( - null} - /> - ); + // const { user } = render( + // null} + // /> + // ); - expect(screen.getByText("Agent")).toBeInTheDocument(); - await user.hover(screen.getByText(orbitVersion)); + // expect(screen.getByText("Agent")).toBeInTheDocument(); + // await user.hover(screen.getByText(orbitVersion)); - expect( - screen.getByText(new RegExp(osqueryVersion, "i")) - ).toBeInTheDocument(); - expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument(); - }); + // expect( + // screen.getByText(new RegExp(osqueryVersion, "i")) + // ).toBeInTheDocument(); + // expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument(); + // }); it("for Chromebooks, render Agent header with osquery_version that is the fleetd chrome version and no tooltip", async () => { const render = createCustomRenderer({ From 1bde2f57a1d9eb4a9b87c6d598bf684ad3f68e6d Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Tue, 26 Nov 2024 19:29:08 -0600 Subject: [PATCH 30/36] Cherry-Pick: Linux OS settings + disk encryption host filter fixes (#24203) Cherry-pick of #24200 into 4.60.0, for #24174 Co-authored-by: Tim Lee --- server/datastore/mysql/hosts.go | 43 +++++++++++--- server/datastore/mysql/hosts_test.go | 84 ++++++++++++++++++++------- server/datastore/mysql/labels_test.go | 4 +- server/datastore/mysql/linux_mdm.go | 34 +++++++++++ 4 files changed, 133 insertions(+), 32 deletions(-) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 0de58ba918a0..659aa12091c2 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1404,21 +1404,39 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis // or are servers. Similar logic could be applied to macOS hosts but is not included in this // current implementation. - sqlFmt := ` AND h.platform IN('windows', 'darwin', 'ios', 'ipados') AND (ne.id IS NOT NULL OR mwe.host_uuid IS NOT NULL) AND hmdm.enrolled = 1` + // TODO once testLabelsListHostsInLabelOSSettings enrolls hosts into the correct MDM, switch to this: + /*sqlFmt := ` AND ( + (h.platform = 'windows' AND mwe.host_uuid IS NOT NULL AND hmdm.enrolled = 1) -- windows + OR (h.platform IN ('darwin', 'ios', 'ipados') AND ne.id IS NOT NULL AND hmdm.enrolled = 1) -- apple + OR (h.platform = 'ubuntu' OR h.os_version LIKE 'Fedora%%') -- linux + )`*/ + + sqlFmt := ` AND ( + (h.platform IN('windows', 'darwin', 'ios', 'ipados') AND (ne.id IS NOT NULL OR mwe.host_uuid IS NOT NULL) AND hmdm.enrolled = 1) + OR (h.platform = 'ubuntu' OR h.os_version LIKE 'Fedora%%') + )` + if opt.TeamFilter == nil { // OS settings filter is not compatible with the "all teams" option so append the "no team" // filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) sqlFmt += ` AND h.team_id IS NULL` } - var whereMacOS, whereWindows string + var whereMacOS, whereWindows, whereLinux string sqlFmt += ` -AND ((h.platform = 'windows' AND (%s)) -OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s)))` +AND ( + (h.platform = 'windows' AND (%s)) + OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s)) + OR ((h.os_version LIKE 'Fedora%%' OR h.platform = 'ubuntu') AND (%s)) +)` // construct the WHERE for macOS whereMacOS = fmt.Sprintf(`(%s) = ?`, sqlCaseMDMAppleStatus()) paramsMacOS := []any{opt.OSSettingsFilter} + // construct the WHERE for linux + whereLinux = fmt.Sprintf(`(%s) = ?`, sqlCaseLinuxOSSettingsStatus()) + paramsLinux := []any{opt.OSSettingsFilter} + // construct the WHERE for windows whereWindows = `hmdm.is_server = 0` paramsWindows := []any{} @@ -1520,8 +1538,9 @@ OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND ( paramsWindows = append(paramsWindows, opt.OSSettingsFilter) params = append(params, paramsWindows...) params = append(params, paramsMacOS...) + params = append(params, paramsLinux...) - return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), params, nil + return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS, whereLinux), params, nil } func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}, enableDiskEncryption bool) (string, []interface{}) { @@ -1529,13 +1548,13 @@ func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt return sql, params } - sqlFmt := " AND h.platform IN('windows', 'darwin')" + sqlFmt := " AND h.platform IN('windows', 'darwin', 'ubuntu', 'rhel')" if opt.TeamFilter == nil { // OS settings filter is not compatible with the "all teams" option so append the "no // team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) sqlFmt += ` AND h.team_id IS NULL` } - sqlFmt += ` AND ((h.platform = 'windows' AND %s) OR (h.platform = 'darwin' AND %s))` + sqlFmt += ` AND ((h.platform = 'windows' AND %s) OR (h.platform = 'darwin' AND %s) OR ((h.platform = 'ubuntu' OR h.os_version LIKE 'Fedora%%') AND %s))` var subqueryMacOS string var subqueryParams []interface{} @@ -1580,7 +1599,10 @@ func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt whereMacOS = "EXISTS (" + subqueryMacOS + ")" } - return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...) + whereLinux := fmt.Sprintf(`(%s) = ?`, sqlCaseLinuxDiskEncryptionStatus()) + subqueryParams = append(subqueryParams, opt.OSSettingsDiskEncryptionFilter) + + return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS, whereLinux), append(params, subqueryParams...) } func filterHostsByMDMBootstrapPackageStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { @@ -3839,16 +3861,19 @@ ON DUPLICATE KEY UPDATE `, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot) return err } + func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool { var pendingEscrowCount uint _ = sqlx.GetContext(ctx, ds.reader(ctx), &pendingEscrowCount, ` SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND reset_requested = TRUE`, hostID) return pendingEscrowCount > 0 } + func (ds *Datastore) ClearPendingEscrow(ctx context.Context, hostID uint) error { _, err := ds.writer(ctx).ExecContext(ctx, `UPDATE host_disk_encryption_keys SET reset_requested = FALSE WHERE host_id = ?`, hostID) return err } + func (ds *Datastore) ReportEscrowError(ctx context.Context, hostID uint, errorMessage string) error { _, err := ds.writer(ctx).ExecContext(ctx, ` INSERT INTO host_disk_encryption_keys @@ -3856,6 +3881,7 @@ INSERT INTO host_disk_encryption_keys `, hostID, errorMessage) return err } + func (ds *Datastore) QueueEscrow(ctx context.Context, hostID uint) error { _, err := ds.writer(ctx).ExecContext(ctx, ` INSERT INTO host_disk_encryption_keys @@ -3863,6 +3889,7 @@ INSERT INTO host_disk_encryption_keys `, hostID) return err } + func (ds *Datastore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error { var hasKeyCount uint err := sqlx.GetContext(ctx, ds.reader(ctx), &hasKeyCount, ` diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index de3fe566e730..51e12566a68e 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -790,6 +790,7 @@ func testHostsDelete(t *testing.T, ds *Datastore) { } func listHostsCheckCount(t *testing.T, ds *Datastore, filter fleet.TeamFilter, opt fleet.HostListOptions, expectedCount int) []*fleet.Host { + t.Helper() hosts, err := ds.ListHosts(context.Background(), filter, opt) require.NoError(t, err) count, err := ds.CountHosts(context.Background(), filter, opt) @@ -809,28 +810,35 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { require.NoError(t, err) var hosts []*fleet.Host - for i := 0; i < 10; i++ { + for i := 0; i < 20; i++ { var opts []test.NewHostOption switch i { - case 5, 6: + case 0: opts = append(opts, test.WithPlatform("windows")) + case 1, 2: + opts = append(opts, test.WithPlatform("ubuntu")) // supported for linux encryption + case 3, 4, 5: + opts = append(opts, test.WithOSVersion("Fedora 33")) // supported for linux encryption + case 6, 7, 8, 9: + opts = append(opts, test.WithPlatform("foo")) // not supported for linux encryption } h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", - fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...) + fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...) // default macos platform hosts = append(hosts, h) nanoEnrollAndSetHostMDMData(t, ds, h, false) } + userFilter := fleet.TeamFilter{User: test.UserAdmin} - // confirm intial state + // confirm initial state listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID}, 0) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) - // assign three hosts to team 1 - require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID})) + // assign three macos hosts to team 1 + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[10].ID, hosts[11].ID, hosts[12].ID})) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)-3) @@ -838,7 +846,7 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) // assign four hosts to team 2 - require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{hosts[3].ID, hosts[4].ID, hosts[5].ID, hosts[6].ID})) + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{hosts[13].ID, hosts[14].ID, hosts[15].ID, hosts[16].ID})) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)-7) @@ -851,7 +859,7 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { { ProfileUUID: profUUID, ProfileIdentifier: "identifier", - HostUUID: hosts[0].UUID, // hosts[0] is assgined to team 1 + HostUUID: hosts[10].UUID, // hosts[10] is assgined to team 1 CommandUUID: "command-uuid-1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying, @@ -869,46 +877,78 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { { ProfileUUID: profUUID, ProfileIdentifier: "identifier", - HostUUID: hosts[9].UUID, // hosts[9] is assgined to no team + HostUUID: hosts[19].UUID, // hosts[19] is assgined to no team CommandUUID: "command-uuid-2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying, Checksum: []byte("csum"), }, })) - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[10] listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team // macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19] - // test team filter in combination with os settings filter - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0] + // OS Settings Filters + + // team 1 + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[10] + + // team 2 listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team + // os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19] listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerifying}, 1) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsPending}, 5) // pending supported linux hosts + + require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1].ID, "key1", "morton", 1)) // set host 1 to verified + require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed + + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerified}, 1) // hosts[1] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsFailed}, 1) // hosts[2] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsPending}, 3) // still-pending supported linux hosts + + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionFailed}, 1) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionActionRequired}, 3) // test team filter in combination with os settings disk encryptionfilter require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { ProfileUUID: profUUID, ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier, - HostUUID: hosts[8].UUID, // hosts[8] is assgined to no team + HostUUID: hosts[18].UUID, // hosts[18] is assgined to no team CommandUUID: "command-uuid-3", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending, Checksum: []byte("disk-encryption-csum"), }, })) - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // hosts[10] listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // wrong team // os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[18] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[18] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[18] + + // move linux hosts to team 1 (un-escrows keys) + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID, hosts[5].ID})) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsPending}, 5) // pending supported linux hosts + + require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1].ID, "key1", "mutton", 2)) // set host 1 to verified + require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed + + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerified}, 1) // hosts[1] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsFailed}, 1) // hosts[2] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsPending}, 3) // still-pending supported linux hosts + + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionFailed}, 1) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionActionRequired}, 3) // Bad team filter _, err = ds.ListHosts(context.Background(), userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterBad}) diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index 52805cecc6a5..8db192ddbaea 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -1568,14 +1568,14 @@ func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) { hosts := listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{}, 3) checkHosts(t, hosts, []uint{h1.ID, h2.ID, h3.ID}) - t.Run("os_settings", func(t *testing.T) { + t.Run("os_settings_disk_encryption", func(t *testing.T) { hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1) checkHosts(t, hosts, []uint{h1.ID}) hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) checkHosts(t, hosts, []uint{h2.ID}) }) - t.Run("os_settings_disk_encryption", func(t *testing.T) { + t.Run("os_settings", func(t *testing.T) { hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerified}, 1) checkHosts(t, hosts, []uint{h1.ID}) hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsPending}, 1) diff --git a/server/datastore/mysql/linux_mdm.go b/server/datastore/mysql/linux_mdm.go index 2cd88843efd2..126cbc0a39a8 100644 --- a/server/datastore/mysql/linux_mdm.go +++ b/server/datastore/mysql/linux_mdm.go @@ -67,3 +67,37 @@ func (ds *Datastore) GetLinuxDiskEncryptionSummary(ctx context.Context, teamID * return summary, nil } + +func sqlCaseLinuxOSSettingsStatus() string { + return ` + CASE WHEN + hdek.base64_encrypted IS NOT NULL + AND hdek.base64_encrypted != '' + AND hdek.client_error = '' THEN + '` + string(fleet.OSSettingsVerified) + `' + WHEN hdek.client_error IS NOT NULL + AND hdek.client_error != '' THEN + '` + string(fleet.OSSettingsFailed) + `' + WHEN hdek.base64_encrypted IS NULL + OR (hdek.base64_encrypted = '' + AND hdek.client_error = '') THEN + '` + string(fleet.OSSettingsPending) + `' + END` +} + +func sqlCaseLinuxDiskEncryptionStatus() string { + return ` + CASE WHEN + hdek.base64_encrypted IS NOT NULL + AND hdek.base64_encrypted != '' + AND hdek.client_error = '' THEN + '` + string(fleet.DiskEncryptionVerified) + `' + WHEN hdek.client_error IS NOT NULL + AND hdek.client_error != '' THEN + '` + string(fleet.DiskEncryptionFailed) + `' + WHEN hdek.base64_encrypted IS NULL + OR (hdek.base64_encrypted = '' + AND hdek.client_error = '') THEN + '` + string(fleet.DiskEncryptionActionRequired) + `' + END` +} From f49d84b4b31658c023ae2124c6364fd6039efe50 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Wed, 27 Nov 2024 11:22:50 -0500 Subject: [PATCH 31/36] fix: add fleet actor for setup experience global activities (#24196) (#24214) Cherry-pick for #24196 --- .../cards/ActivityFeed/ActivityItem/ActivityItem.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index 6318b9ce8957..b162a22fcc9f 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -1385,6 +1385,14 @@ const ActivityItem = ({ ? addGravatarUrlToResource({ email: actor_email }) : { gravatar_url: DEFAULT_GRAVATAR_LINK }; + if ( + !activity.actor_email && + !activity.actor_full_name && + !activity.actor_id + ) { + activity.actor_full_name = "Fleet"; + } + const activityCreatedAt = new Date(activity.created_at); const indicatePremiumFeature = isSandboxMode && PREMIUM_ACTIVITIES.has(activity.type); From 963ad268c95ee0cc349ee74a202e1d72ba6e61d8 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Wed, 27 Nov 2024 11:13:23 -0600 Subject: [PATCH 32/36] cherry pick build fix (#24218) Co-authored-by: Lucas Manuel Rodriguez --- tools/fleetctl-docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fleetctl-docker/Dockerfile b/tools/fleetctl-docker/Dockerfile index 6b82ed628541..ca678cedf7fa 100644 --- a/tools/fleetctl-docker/Dockerfile +++ b/tools/fleetctl-docker/Dockerfile @@ -2,7 +2,7 @@ FROM rust:latest@sha256:56418f03475cf7b107f87d7fabe99ce9a4a9f9904daafa99be7c50d9 ARG transporter_url=https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/resources/download/public/Transporter__Linux/bin -RUN cargo install --version 0.16.0 apple-codesign \ +RUN cargo install --locked --version 0.16.0 apple-codesign \ && curl -sSf $transporter_url -o transporter_install.sh \ && sh transporter_install.sh --target transporter --accept --noexec From 00112e6ccc50242f6bf62859b9fb32744f085db5 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 27 Nov 2024 12:56:51 -0500 Subject: [PATCH 33/36] Cherry-pick of #24207 for #24024 fix (#24219) --- .../24024-bypass-setup-experience-if-empty | 2 + server/mdm/lifecycle/lifecycle.go | 17 ++-- server/service/apple_mdm.go | 13 +-- server/service/integration_mdm_dep_test.go | 83 +++++++++++++++++-- server/service/integration_mdm_test.go | 8 ++ server/service/orbit.go | 16 ++-- server/worker/apple_mdm.go | 45 +++++----- server/worker/apple_mdm_test.go | 58 ++++++++++--- 8 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 changes/24024-bypass-setup-experience-if-empty diff --git a/changes/24024-bypass-setup-experience-if-empty b/changes/24024-bypass-setup-experience-if-empty new file mode 100644 index 000000000000..319df88c1c91 --- /dev/null +++ b/changes/24024-bypass-setup-experience-if-empty @@ -0,0 +1,2 @@ +* Bypass the setup experience UI if there is no setup experience item to process (no software to install, no script to execute), so that releasing the device is done without going through that window. +* Fixed releasing a DEP-enrolled macOS device if mTLS is configured for `fleetd`. diff --git a/server/mdm/lifecycle/lifecycle.go b/server/mdm/lifecycle/lifecycle.go index 33658a23678d..fd96454274f3 100644 --- a/server/mdm/lifecycle/lifecycle.go +++ b/server/mdm/lifecycle/lifecycle.go @@ -32,13 +32,14 @@ const ( // Not all options are required for all actions, each individual action should // validate that it receives the required information. type HostOptions struct { - Action HostAction - Platform string - UUID string - HardwareSerial string - HardwareModel string - EnrollReference string - Host *fleet.Host + Action HostAction + Platform string + UUID string + HardwareSerial string + HardwareModel string + EnrollReference string + Host *fleet.Host + HasSetupExperienceItems bool } // HostLifecycle manages MDM host lifecycle actions @@ -174,6 +175,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro opts.Platform, tmID, opts.EnrollReference, + !opts.HasSetupExperienceItems, ) return ctxerr.Wrap(ctx, err, "queue DEP post-enroll task") } @@ -189,6 +191,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro opts.Platform, tmID, opts.EnrollReference, + false, ); err != nil { return ctxerr.Wrap(ctx, err, "queue manual post-enroll task") } diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 6ad19bb1eb97..c0eb9a7a1f43 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2769,20 +2769,21 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. return ctxerr.Wrap(r.Context, err, "cleaning SCEP refs") } + var hasSetupExpItems bool if m.AwaitingConfiguration { // Enqueue setup experience items and mark the host as being in setup experience - _, err := svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID) + hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID) if err != nil { return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks") } - } return svc.mdmLifecycle.Do(r.Context, mdmlifecycle.HostOptions{ - Action: mdmlifecycle.HostActionTurnOn, - Platform: info.Platform, - UUID: r.ID, - EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey], + Action: mdmlifecycle.HostActionTurnOn, + Platform: info.Platform, + UUID: r.ID, + EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey], + HasSetupExperienceItems: hasSetupExpItems, }) } diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index fbdbc4541ac4..0cb740680d8b 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -121,12 +121,33 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceGlobal() { s.enableABM("fleet_ade_test") + // add a setup experience script to run for no team + extraArgs := make(map[string][]string) + body, headers := generateNewScriptMultipartRequest(t, + "script.sh", []byte(`echo "hello"`), s.token, extraArgs) + s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers) + + // test manual and automatic release with the new setup experience flow + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false) + }) + } // test manual and automatic release with the old worker flow for _, enableReleaseManually := range []bool{false, true} { - t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { + t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) { s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", true) }) } + + // remove the setup experience script, run the new setup experience flow when + // there is no setup experience item to process (so it is bypassed) + s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK) + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false) + }) + } } func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() { @@ -211,12 +232,35 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() { // enable FileVault s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", json.RawMessage([]byte(fmt.Sprintf(`{"enable_disk_encryption":true,"team_id":%d}`, tm.ID))), http.StatusNoContent) + // add a setup experience script to run for this team + extraArgs := map[string][]string{ + "team_id": {fmt.Sprintf("%d", tm.ID)}, + } + body, headers := generateNewScriptMultipartRequest(t, + "script.sh", []byte(`echo "hello"`), s.token, extraArgs) + s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers) + + // test manual and automatic release with the new setup experience flow + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false) + }) + } // test manual and automatic release with the old worker flow for _, enableReleaseManually := range []bool{false, true} { - t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) { + t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) { s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", true) }) } + + // remove the setup experience script, run the new setup experience flow when + // there is no setup experience item to process (so it is bypassed) + s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK, "team_id", fmt.Sprint(tm.ID)) + for _, enableReleaseManually := range []bool{false, true} { + t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) { + s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false) + }) + } } func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() { @@ -286,6 +330,11 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() { func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, device godep.Device, enableReleaseManually bool, teamID *uint, customProfileIdent string, useOldFleetdFlow bool) { ctx := context.Background() + var isIphone bool + if device.DeviceFamily == "iPhone" { + isIphone = true + } + // set the enable release device manually option payload := map[string]any{ "enable_release_device_manually": enableReleaseManually, @@ -359,15 +408,22 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de // enroll the host depURLToken := loadEnrollmentProfileDEPToken(t, s.ds) mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken) - var isIphone bool - if device.DeviceFamily == "iPhone" { + if isIphone { mdmDevice.Model = "iPhone 14,6" - isIphone = true } mdmDevice.SerialNumber = device.SerialNumber err := mdmDevice.Enroll() require.NoError(t, err) + // check if it has setup experience items or not + hasSetupExpItems := true + _, err = s.ds.GetHostAwaitingConfiguration(ctx, mdmDevice.UUID) + if fleet.IsNotFound(err) { + hasSetupExpItems = false + } else if err != nil { + require.NoError(t, err) + } + // run the worker to process the DEP enroll request s.runWorker() // run the cron to assign configuration profiles @@ -525,8 +581,13 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de b, err := io.ReadAll(res.Body) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, &orbitConfigResp)) - // should be notified of the setup experience flow - require.False(t, orbitConfigResp.Notifications.RunSetupExperience) + if hasSetupExpItems { + // should be notified of the setup experience flow + require.True(t, orbitConfigResp.Notifications.RunSetupExperience) + } else { + // should bypass the setup experience flow + require.False(t, orbitConfigResp.Notifications.RunSetupExperience) + } if enableReleaseManually { // get the worker's pending job from the future, there should not be any @@ -537,7 +598,7 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de return } - if useOldFleetdFlow { + if useOldFleetdFlow || !hasSetupExpItems { // there should be a Release Device pending job pending, err := s.ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute)) require.NoError(t, err) @@ -574,6 +635,12 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de require.NoError(t, err) require.Len(t, pending, 0) + // mark the setup experience script as done + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE setup_experience_status_results SET status = 'success' WHERE host_uuid = ?`, mdmDevice.UUID) + return err + }) + // call the /status endpoint to automatically release the host var statusResp getOrbitSetupExperienceStatusResponse s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 5e4bafe64a39..c0177ab049b2 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -677,6 +677,14 @@ func (s *integrationMDMTestSuite) TearDownTest() { _, err := tx.ExecContext(ctx, "DELETE FROM vpp_tokens;") return err }) + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_status_results;") + return err + }) + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_scripts;") + return err + }) } func (s *integrationMDMTestSuite) mockDEPResponse(orgName string, handler http.Handler) { diff --git a/server/service/orbit.go b/server/service/orbit.go index 1b3b700b0ec5..af5c69690e2f 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -249,15 +249,13 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro notifs.RunSetupExperience = true } - if inSetupAssistant || fleet.IsNotFound(err) { - // If the client is running a fleetd that doesn't support setup experience, or if no - // software/script has been configured for setup experience, then we should fall back to - // the "old way" of releasing the device. We do an additional check for - // !inSetupAssistant to prevent enqueuing a new job every time the /config - // endpoint is hit. + if inSetupAssistant { + // If the client is running a fleetd that doesn't support setup + // experience, then we should fall back to the "old way" of releasing + // the device. mp, ok := capabilities.FromContext(ctx) - if !ok || !mp.Has(fleet.CapabilitySetupExperience) || !inSetupAssistant { - level.Debug(svc.logger).Log("msg", "host doesn't support setup experience or no setup experience configured, falling back to worker-based device release", "host_uuid", host.UUID) + if !ok || !mp.Has(fleet.CapabilitySetupExperience) { + level.Debug(svc.logger).Log("msg", "host doesn't support setup experience, falling back to worker-based device release", "host_uuid", host.UUID) if err := svc.processReleaseDeviceForOldFleetd(ctx, host); err != nil { return fleet.OrbitConfig{}, err } @@ -521,7 +519,7 @@ func (svc *Service) processReleaseDeviceForOldFleetd(ctx context.Context, host * // Enroll reference arg is not used in the release device task, passing empty string. if err := worker.QueueAppleMDMJob(ctx, svc.ds, svc.logger, worker.AppleMDMPostDEPReleaseDeviceTask, - host.UUID, host.Platform, host.TeamID, "", bootstrapCmdUUID, acctConfigCmdUUID); err != nil { + host.UUID, host.Platform, host.TeamID, "", false, bootstrapCmdUUID, acctConfigCmdUUID); err != nil { return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job") } } diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index 01ac59ea79f9..235a3a7333d7 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -50,12 +50,13 @@ func (a *AppleMDM) Name() string { // appleMDMArgs is the payload for the Apple MDM job. type appleMDMArgs struct { - Task AppleMDMTask `json:"task"` - HostUUID string `json:"host_uuid"` - TeamID *uint `json:"team_id,omitempty"` - EnrollReference string `json:"enroll_reference,omitempty"` - EnrollmentCommands []string `json:"enrollment_commands,omitempty"` - Platform string `json:"platform,omitempty"` + Task AppleMDMTask `json:"task"` + HostUUID string `json:"host_uuid"` + TeamID *uint `json:"team_id,omitempty"` + EnrollReference string `json:"enroll_reference,omitempty"` + EnrollmentCommands []string `json:"enrollment_commands,omitempty"` + Platform string `json:"platform,omitempty"` + UseWorkerDeviceRelease bool `json:"use_worker_device_release,omitempty"` } // Run executes the apple_mdm job. @@ -163,9 +164,10 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) } } - // proceed to release the device only if it is not a macos, as those are - // released via the setup experience flow. - if !isMacOS(args.Platform) { + // proceed to release the device if it is not a macos, as those are released + // via the setup experience flow, or if we were told to use the worker based + // release. + if !isMacOS(args.Platform) || args.UseWorkerDeviceRelease { var manualRelease bool if args.TeamID == nil { ac, err := a.Datastore.AppConfig(ctx) @@ -187,7 +189,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) // be final and same for MDM profiles of that host; it means the DEP // enrollment process is done and the device can be released. if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask, - args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil { + args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, false, awaitCmdUUIDs...); err != nil { return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job") } } @@ -198,10 +200,11 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) // This job is deprecated for macos because releasing devices is now done via // the orbit endpoint /setup_experience/status that is polled by a swift dialog -// UI window during the setup process, and automatically releases the device -// once all pending setup tasks are done. However, it must remain implemented -// for iOS and iPadOS and in case there are such jobs to process after a Fleet -// migration to a new version. +// UI window during the setup process (unless there are no setup experience +// items, in which case this worker job is used), and automatically releases +// the device once all pending setup tasks are done. However, it must remain +// implemented for iOS and iPadOS and in case there are such jobs to process +// after a Fleet migration to a new version. func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error { // Edge cases: // - if the device goes offline for a long time, should we go ahead and @@ -355,6 +358,7 @@ func QueueAppleMDMJob( platform string, teamID *uint, enrollReference string, + useWorkerDeviceRelease bool, enrollmentCommandUUIDs ...string, ) error { attrs := []interface{}{ @@ -373,12 +377,13 @@ func QueueAppleMDMJob( level.Info(logger).Log(attrs...) args := &appleMDMArgs{ - Task: task, - HostUUID: hostUUID, - TeamID: teamID, - EnrollReference: enrollReference, - EnrollmentCommands: enrollmentCommandUUIDs, - Platform: platform, + Task: task, + HostUUID: hostUUID, + TeamID: teamID, + EnrollReference: enrollReference, + EnrollmentCommands: enrollmentCommandUUIDs, + Platform: platform, + UseWorkerDeviceRelease: useWorkerDeviceRelease, } // the release device task is always added with a delay diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index 8b497379aba0..f27aa32bf15a 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -141,7 +141,7 @@ func TestAppleMDM(t *testing.T) { // create a host and enqueue the job h := createEnrolledHost(t, 1, nil, true) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should mark the job as done @@ -171,7 +171,7 @@ func TestAppleMDM(t *testing.T) { // create a host and enqueue the job h := createEnrolledHost(t, 1, nil, true) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should mark the job as failed @@ -204,7 +204,7 @@ func TestAppleMDM(t *testing.T) { w.Register(mdmWorker) // use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support. - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -239,7 +239,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -281,7 +281,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -330,7 +330,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false) require.NoError(t, err) // run the worker, should succeed @@ -380,7 +380,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false) require.NoError(t, err) // run the worker, should succeed @@ -418,7 +418,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd", false) require.NoError(t, err) // run the worker, should succeed @@ -461,7 +461,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID) + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID, false) require.NoError(t, err) // run the worker, should succeed @@ -514,7 +514,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID) + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID, false) require.NoError(t, err) // run the worker, should succeed @@ -548,7 +548,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "", false) require.NoError(t, err) // run the worker, should succeed @@ -564,4 +564,40 @@ func TestAppleMDM(t *testing.T) { require.Empty(t, jobs) require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t)) }) + + t.Run("use worker for automatic release", func(t *testing.T) { + mysql.SetTestABMAssets(t, ds, testOrgName) + defer mysql.TruncateTables(t, ds) + + h := createEnrolledHost(t, 1, nil, true) + + mdmWorker := &AppleMDM{ + Datastore: ds, + Log: nopLog, + Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}), + } + w := NewWorker(ds, nopLog) + w.Register(mdmWorker) + + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true) + require.NoError(t, err) + + // run the worker, should succeed + err = w.ProcessJobs(ctx) + require.NoError(t, err) + + // ensure the job's not_before allows it to be returned if it were to run + // again + time.Sleep(time.Second) + + require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t)) + + // the release device job got enqueued + jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().Add(time.Minute)) // release job is always added with a delay + require.NoError(t, err) + require.Len(t, jobs, 1) + require.Equal(t, fleet.JobStateQueued, jobs[0].State) + require.Equal(t, appleMDMJobName, jobs[0].Name) + require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask) + }) } From 00278c7c296fecda463b18c62abb7551db5ae83b Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Wed, 27 Nov 2024 14:29:47 -0600 Subject: [PATCH 34/36] Adding changes for Fleet v4.60.0 (#23817) Co-authored-by: Lucas Manuel Rodriguez --- CHANGELOG.md | 49 +++++++++++++++++++ changes/14899-yara-rules | 1 - ...5-improve-memory-usage-software-installers | 1 - changes/21338-scope-profile-pending-rebuild | 1 - .../21633-windows-auto-enrollment-info-banner | 1 - changes/21709-activities-automation-activity | 1 - changes/21888-dequeue-pending-scripts | 1 - .../22162-exclude-labels-fix-default-behavior | 1 - changes/22187-gitops-software-relative-paths | 1 - changes/22224-query-log-destinations | 1 - changes/22269-software-title-updated-at | 1 - changes/22359-gitops-mult-abm | 2 - changes/22361-os-update-ade-sso | 2 - changes/22437-linux-lock-black-screen | 1 - changes/22446-scripts-modal | 1 - changes/22575-ui-for-include-any-labels | 2 - changes/22576-labels-include-any-gitops | 1 - changes/22578-db-schema | 1 - changes/22581-cron-updates | 1 - changes/22606-keyboard-accessiblity | 1 - changes/22702-linux-encryption-frontend | 1 - changes/22773-fma-uninstall-fix | 1 - changes/22810-fleetd-enroll-activity | 1 - changes/22891-zstd-deb-packages | 1 - changes/22985-disable-forms-keyboard-access | 1 - .../23016-add-chrome-host-text-area-height | 2 - changes/23021-abm-cert-pem | 1 - changes/23078-allow-skipping-vuln-details | 1 - ...te-mock-service-worker-package-for-secutiy | 1 - changes/23200-ade-enroll | 2 - changes/23213-okta-verify | 1 - changes/23247-vpp-app-install | 2 - changes/23492-software-batch-status-code | 1 - changes/23525-ndes-errors | 1 - changes/23540-pe-sfx | 1 - ...-create-update-label-returns-outdated-info | 1 - changes/23651-reenter-password | 1 - ...3669-dismiss-error-flash-on-url-change-dup | 1 - changes/24024-no-setup-exp | 2 - changes/8750-add-team_identifier-to-software | 1 - charts/fleet/Chart.yaml | 4 +- charts/fleet/values.yaml | 2 +- .../dogfood/terraform/aws/variables.tf | 2 +- .../dogfood/terraform/gcp/variables.tf | 2 +- infrastructure/guardduty/.terraform.lock.hcl | 4 +- infrastructure/guardduty/main.tf | 2 +- .../infrastructure/cloudtrail/main.tf | 2 +- .../elastic-agent/.terraform.lock.hcl | 4 +- .../infrastructure/elastic-agent/main.tf | 2 +- .../guardduty-alerts/.terraform.lock.hcl | 4 +- .../infrastructure/guardduty-alerts/main.tf | 2 +- .../infrastructure/spend_alerts/main.tf | 2 +- terraform/addons/vuln-processing/variables.tf | 4 +- terraform/byo-vpc/byo-db/byo-ecs/variables.tf | 4 +- terraform/byo-vpc/byo-db/variables.tf | 4 +- terraform/byo-vpc/example/main.tf | 2 +- terraform/byo-vpc/variables.tf | 4 +- terraform/example/main.tf | 4 +- terraform/variables.tf | 4 +- tools/fleetctl-npm/package.json | 2 +- 60 files changed, 79 insertions(+), 76 deletions(-) delete mode 100644 changes/14899-yara-rules delete mode 100644 changes/20595-improve-memory-usage-software-installers delete mode 100644 changes/21338-scope-profile-pending-rebuild delete mode 100644 changes/21633-windows-auto-enrollment-info-banner delete mode 100644 changes/21709-activities-automation-activity delete mode 100644 changes/21888-dequeue-pending-scripts delete mode 100644 changes/22162-exclude-labels-fix-default-behavior delete mode 100644 changes/22187-gitops-software-relative-paths delete mode 100644 changes/22224-query-log-destinations delete mode 100644 changes/22269-software-title-updated-at delete mode 100644 changes/22359-gitops-mult-abm delete mode 100644 changes/22361-os-update-ade-sso delete mode 100644 changes/22437-linux-lock-black-screen delete mode 100644 changes/22446-scripts-modal delete mode 100644 changes/22575-ui-for-include-any-labels delete mode 100644 changes/22576-labels-include-any-gitops delete mode 100644 changes/22578-db-schema delete mode 100644 changes/22581-cron-updates delete mode 100644 changes/22606-keyboard-accessiblity delete mode 100644 changes/22702-linux-encryption-frontend delete mode 100644 changes/22773-fma-uninstall-fix delete mode 100644 changes/22810-fleetd-enroll-activity delete mode 100644 changes/22891-zstd-deb-packages delete mode 100644 changes/22985-disable-forms-keyboard-access delete mode 100644 changes/23016-add-chrome-host-text-area-height delete mode 100644 changes/23021-abm-cert-pem delete mode 100644 changes/23078-allow-skipping-vuln-details delete mode 100644 changes/23128-update-mock-service-worker-package-for-secutiy delete mode 100644 changes/23200-ade-enroll delete mode 100644 changes/23213-okta-verify delete mode 100644 changes/23247-vpp-app-install delete mode 100644 changes/23492-software-batch-status-code delete mode 100644 changes/23525-ndes-errors delete mode 100644 changes/23540-pe-sfx delete mode 100644 changes/23597-fix-create-update-label-returns-outdated-info delete mode 100644 changes/23651-reenter-password delete mode 100644 changes/23669-dismiss-error-flash-on-url-change-dup delete mode 100644 changes/24024-no-setup-exp delete mode 100644 changes/8750-add-team_identifier-to-software diff --git a/CHANGELOG.md b/CHANGELOG.md index ec554cea3507..bf3831e84a0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +## Fleet 4.60.0 (Nov 27, 2024) + +### Endpoint operations +- Added support for labels_include_any to gitops. +- Added major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation). +- Added activity item for `fleetd` enrollment with host serial and display name. +- Added capability for Fleet to serve YARA rules to agents over HTTPS authenticated via node key (requires osquery 5.14+). +- Added a query to allow users to turn on/off automations while being transparent of the current log destination. +- Updated UI to allow users to view scripts (from both the scripts page and host details page) without downloading them. +- Updated activity feed to generate an activity when activity automations are enabled, edited, or disabled. +- Cancelled pending script executions when a script is edited or deleted. + +### Device management (MDM) +- Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy. +- Added info banner for cloud customers to help with their windows autoenrollment setup. +- Added DB support for "include any" label profile deployment. +- Added support for "include any" label/profile relationships to the profile reconciliation machinery. +- Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint. +- Added indicator of how fresh a software title's host and version counts are on the title's details page. +- Added UI for allowing users to install custom profiles on hosts that include any of the defined labels. +- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. +- Added support for deb packages compressed with zstd. + +### Vulnerability management +- Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`). + +### Bug fixes and improvements +- Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required. +- Improved performance of adding and removing profiles to large teams by an order of magnitude. +- Disabled accessibility via keyboard for forms that are disabled via a slider. +- Updated software batch endpoint status code from 200 (OK) to 202 (Accepted). +- Updated a package used for testing (msw) to improve security. +- Updated to reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04. +- Updated GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there are more than 1 ABM tokens in Fleet. +- Dismissed error flash on the my device page when navigating to another URL. +- Modified the Fleet setup experience feature to not run if there is no software or script configured for the setup experience. +- Set a more accurate minimum height for the Add hosts > ChromeOS > Policy for extension field, avoiding a scrollbar. +- Added UI prompt for user to reenter the password if SCEP/NDES url or username has changed. +- Updated ABM public key to download as as PEM format instead of CRT. +- Fixed issue with uploading macOS software packages that do not have a top level `Distribution.xml`, but do have a top level `PackageInfo.xml`. For example, Okta Verify.app. +- Fixed some cases where Fleet Maintained Apps generated incorrect uninstall scripts. +- Fixed a bug where a device that was removed from ABM and then added back wouldn't properly re-enroll in Fleet MDM. +- Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera. +- Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica. +- Fixed the MDM configuration profiles deployment when based on excluded labels. +- Fixed gitops path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere). +- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM IdP integration was enabled. +- Fixed a bug where users would be allowed to attempt an install of an App Store app on a host that was not MDM enrolled. + ## Fleet 4.59.1 (Nov 18, 2024) ### Bug fixes diff --git a/changes/14899-yara-rules b/changes/14899-yara-rules deleted file mode 100644 index 2c92188cfc02..000000000000 --- a/changes/14899-yara-rules +++ /dev/null @@ -1 +0,0 @@ -* Added capability for Fleet to serve yara rules to agents over HTTPS authenticated via node key (requires osquery 5.14+). \ No newline at end of file diff --git a/changes/20595-improve-memory-usage-software-installers b/changes/20595-improve-memory-usage-software-installers deleted file mode 100644 index 7e15f3b935de..000000000000 --- a/changes/20595-improve-memory-usage-software-installers +++ /dev/null @@ -1 +0,0 @@ -* Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required. diff --git a/changes/21338-scope-profile-pending-rebuild b/changes/21338-scope-profile-pending-rebuild deleted file mode 100644 index 59e48839557e..000000000000 --- a/changes/21338-scope-profile-pending-rebuild +++ /dev/null @@ -1 +0,0 @@ -- Speed up adding and removing profiles to large teams by an order of magnitude diff --git a/changes/21633-windows-auto-enrollment-info-banner b/changes/21633-windows-auto-enrollment-info-banner deleted file mode 100644 index 86cdfafdaf8f..000000000000 --- a/changes/21633-windows-auto-enrollment-info-banner +++ /dev/null @@ -1 +0,0 @@ -- add info banner for cloud customers to help with their windows autoenrollment setup diff --git a/changes/21709-activities-automation-activity b/changes/21709-activities-automation-activity deleted file mode 100644 index bc47a6e27330..000000000000 --- a/changes/21709-activities-automation-activity +++ /dev/null @@ -1 +0,0 @@ -* Generate an activity when activity automations are enabled, edited, or disabled. diff --git a/changes/21888-dequeue-pending-scripts b/changes/21888-dequeue-pending-scripts deleted file mode 100644 index 3852ee09c3e4..000000000000 --- a/changes/21888-dequeue-pending-scripts +++ /dev/null @@ -1 +0,0 @@ -* Cancelled pending script executions when a script is edited or deleted. diff --git a/changes/22162-exclude-labels-fix-default-behavior b/changes/22162-exclude-labels-fix-default-behavior deleted file mode 100644 index 41524c8c0399..000000000000 --- a/changes/22162-exclude-labels-fix-default-behavior +++ /dev/null @@ -1 +0,0 @@ -* Fixed the MDM configuration profiles deployment when based on excluded labels - prior to this fix, hosts were considered "not a member" of the label by default, even if they had not yet returned results for the excluded labels. The fix checks the label's creation time vs the host's last reported label results timestamp to prevent deploying a configuration profile if it does not yet know if the host is a member or not of those labels. diff --git a/changes/22187-gitops-software-relative-paths b/changes/22187-gitops-software-relative-paths deleted file mode 100644 index 8f1ce8f480ca..000000000000 --- a/changes/22187-gitops-software-relative-paths +++ /dev/null @@ -1 +0,0 @@ -* GitOps: Fixed path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere). \ No newline at end of file diff --git a/changes/22224-query-log-destinations b/changes/22224-query-log-destinations deleted file mode 100644 index b6172a331b0e..000000000000 --- a/changes/22224-query-log-destinations +++ /dev/null @@ -1 +0,0 @@ -- Creating a query allow users to turn on/off automations while being transparent of the current log destination diff --git a/changes/22269-software-title-updated-at b/changes/22269-software-title-updated-at deleted file mode 100644 index dfc3f127697d..000000000000 --- a/changes/22269-software-title-updated-at +++ /dev/null @@ -1 +0,0 @@ -* Added indicator of how fresh a software title's host and version counts are on the title's details page diff --git a/changes/22359-gitops-mult-abm b/changes/22359-gitops-mult-abm deleted file mode 100644 index b7a7801edbf2..000000000000 --- a/changes/22359-gitops-mult-abm +++ /dev/null @@ -1,2 +0,0 @@ -- Updates GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there - are more than 1 ABM tokens in Fleet. \ No newline at end of file diff --git a/changes/22361-os-update-ade-sso b/changes/22361-os-update-ade-sso deleted file mode 100644 index 40221866fb93..000000000000 --- a/changes/22361-os-update-ade-sso +++ /dev/null @@ -1,2 +0,0 @@ -- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM - IdP integration was enabled. diff --git a/changes/22437-linux-lock-black-screen b/changes/22437-linux-lock-black-screen deleted file mode 100644 index edfd4dc8d477..000000000000 --- a/changes/22437-linux-lock-black-screen +++ /dev/null @@ -1 +0,0 @@ -- Reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04 diff --git a/changes/22446-scripts-modal b/changes/22446-scripts-modal deleted file mode 100644 index 1e06aea93108..000000000000 --- a/changes/22446-scripts-modal +++ /dev/null @@ -1 +0,0 @@ -- Users can view scripts in the UI (from both the scripts page and host details page) without downloading them diff --git a/changes/22575-ui-for-include-any-labels b/changes/22575-ui-for-include-any-labels deleted file mode 100644 index 5f66f8396b79..000000000000 --- a/changes/22575-ui-for-include-any-labels +++ /dev/null @@ -1,2 +0,0 @@ -- add UI for allowing users to install custom profiles on hosts that include any of the defined -labels diff --git a/changes/22576-labels-include-any-gitops b/changes/22576-labels-include-any-gitops deleted file mode 100644 index 228171c7d161..000000000000 --- a/changes/22576-labels-include-any-gitops +++ /dev/null @@ -1 +0,0 @@ -- Add support for labels_include_any to gitops diff --git a/changes/22578-db-schema b/changes/22578-db-schema deleted file mode 100644 index 281c14a6b909..000000000000 --- a/changes/22578-db-schema +++ /dev/null @@ -1 +0,0 @@ -- Adds DB support for "include any" label profile deployment \ No newline at end of file diff --git a/changes/22581-cron-updates b/changes/22581-cron-updates deleted file mode 100644 index f228460a0406..000000000000 --- a/changes/22581-cron-updates +++ /dev/null @@ -1 +0,0 @@ -- Adds support for "include any" label/profile relationships to the profile reconciliation machinery. \ No newline at end of file diff --git a/changes/22606-keyboard-accessiblity b/changes/22606-keyboard-accessiblity deleted file mode 100644 index 6f863e248a7a..000000000000 --- a/changes/22606-keyboard-accessiblity +++ /dev/null @@ -1 +0,0 @@ -- Fleet UI: Major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation) \ No newline at end of file diff --git a/changes/22702-linux-encryption-frontend b/changes/22702-linux-encryption-frontend deleted file mode 100644 index a35d2423751b..000000000000 --- a/changes/22702-linux-encryption-frontend +++ /dev/null @@ -1 +0,0 @@ -- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. diff --git a/changes/22773-fma-uninstall-fix b/changes/22773-fma-uninstall-fix deleted file mode 100644 index 74c4390533b5..000000000000 --- a/changes/22773-fma-uninstall-fix +++ /dev/null @@ -1 +0,0 @@ -- Fix some cases where Fleet Maintained Apps generated incorrect uninstall scripts diff --git a/changes/22810-fleetd-enroll-activity b/changes/22810-fleetd-enroll-activity deleted file mode 100644 index b9b9380a05df..000000000000 --- a/changes/22810-fleetd-enroll-activity +++ /dev/null @@ -1 +0,0 @@ -Added activity item for fleetd enrollment with host serial and display name. diff --git a/changes/22891-zstd-deb-packages b/changes/22891-zstd-deb-packages deleted file mode 100644 index f523dd62720d..000000000000 --- a/changes/22891-zstd-deb-packages +++ /dev/null @@ -1 +0,0 @@ -- Add support for deb packages compressed with zstd diff --git a/changes/22985-disable-forms-keyboard-access b/changes/22985-disable-forms-keyboard-access deleted file mode 100644 index 2e90b69dc543..000000000000 --- a/changes/22985-disable-forms-keyboard-access +++ /dev/null @@ -1 +0,0 @@ -- Fleet UI: Disable accessibility via keyboard for forms that are disabled via a slider diff --git a/changes/23016-add-chrome-host-text-area-height b/changes/23016-add-chrome-host-text-area-height deleted file mode 100644 index 7616f4bfa0a2..000000000000 --- a/changes/23016-add-chrome-host-text-area-height +++ /dev/null @@ -1,2 +0,0 @@ -* Set a more elegant minimum height for the Add hosts > ChromeOS > Policy for extension field, -avoiding a scrollbar. diff --git a/changes/23021-abm-cert-pem b/changes/23021-abm-cert-pem deleted file mode 100644 index c1890e07bb29..000000000000 --- a/changes/23021-abm-cert-pem +++ /dev/null @@ -1 +0,0 @@ -- Download ABM public key as PEM format instead of CRT diff --git a/changes/23078-allow-skipping-vuln-details b/changes/23078-allow-skipping-vuln-details deleted file mode 100644 index 7a299339769b..000000000000 --- a/changes/23078-allow-skipping-vuln-details +++ /dev/null @@ -1 +0,0 @@ -* Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`) \ No newline at end of file diff --git a/changes/23128-update-mock-service-worker-package-for-secutiy b/changes/23128-update-mock-service-worker-package-for-secutiy deleted file mode 100644 index aa9a3e47af24..000000000000 --- a/changes/23128-update-mock-service-worker-package-for-secutiy +++ /dev/null @@ -1 +0,0 @@ -- update a package used for testing (msw) to improve security diff --git a/changes/23200-ade-enroll b/changes/23200-ade-enroll deleted file mode 100644 index 6a6c597bf480..000000000000 --- a/changes/23200-ade-enroll +++ /dev/null @@ -1,2 +0,0 @@ -- Fixes a bug where a device that was removed from ABM and then added back wouldn't properly - re-enroll in Fleet MDM \ No newline at end of file diff --git a/changes/23213-okta-verify b/changes/23213-okta-verify deleted file mode 100644 index 6fd38a9e476b..000000000000 --- a/changes/23213-okta-verify +++ /dev/null @@ -1 +0,0 @@ -Fixed issue with uploading macOS software packages that do not have a top level Distribution.xml, but do have a top level PackageInfo.xml. For example, Okta Verify.app diff --git a/changes/23247-vpp-app-install b/changes/23247-vpp-app-install deleted file mode 100644 index 97a62eb9df09..000000000000 --- a/changes/23247-vpp-app-install +++ /dev/null @@ -1,2 +0,0 @@ -- Fixes a bug where users would be allowed to attempt an install of an App Store app on a host that - was not MDM enrolled. \ No newline at end of file diff --git a/changes/23492-software-batch-status-code b/changes/23492-software-batch-status-code deleted file mode 100644 index 9ab51770d9a4..000000000000 --- a/changes/23492-software-batch-status-code +++ /dev/null @@ -1 +0,0 @@ -* Updated software batch endpoint status code from 200 (OK) to 202 (Accepted) \ No newline at end of file diff --git a/changes/23525-ndes-errors b/changes/23525-ndes-errors deleted file mode 100644 index 409723e8095c..000000000000 --- a/changes/23525-ndes-errors +++ /dev/null @@ -1 +0,0 @@ -Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy. diff --git a/changes/23540-pe-sfx b/changes/23540-pe-sfx deleted file mode 100644 index 63c241a8be83..000000000000 --- a/changes/23540-pe-sfx +++ /dev/null @@ -1 +0,0 @@ -Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera. diff --git a/changes/23597-fix-create-update-label-returns-outdated-info b/changes/23597-fix-create-update-label-returns-outdated-info deleted file mode 100644 index 3a5e26e5aa8c..000000000000 --- a/changes/23597-fix-create-update-label-returns-outdated-info +++ /dev/null @@ -1 +0,0 @@ -* Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica. diff --git a/changes/23651-reenter-password b/changes/23651-reenter-password deleted file mode 100644 index b3fc7df44d87..000000000000 --- a/changes/23651-reenter-password +++ /dev/null @@ -1 +0,0 @@ -- Fleet UI: Prompt user to reenter the password if SCEP/NDES url or username has changed diff --git a/changes/23669-dismiss-error-flash-on-url-change-dup b/changes/23669-dismiss-error-flash-on-url-change-dup deleted file mode 100644 index 125774f81fe3..000000000000 --- a/changes/23669-dismiss-error-flash-on-url-change-dup +++ /dev/null @@ -1 +0,0 @@ -* Dismiss error flash on the my device page when navigating to another URL. \ No newline at end of file diff --git a/changes/24024-no-setup-exp b/changes/24024-no-setup-exp deleted file mode 100644 index 44ab42bcf059..000000000000 --- a/changes/24024-no-setup-exp +++ /dev/null @@ -1,2 +0,0 @@ -- Modifies the Fleet setup experience feature to not run if there is no software or script - configured for the setup experience. \ No newline at end of file diff --git a/changes/8750-add-team_identifier-to-software b/changes/8750-add-team_identifier-to-software deleted file mode 100644 index 0d05d81b0944..000000000000 --- a/changes/8750-add-team_identifier-to-software +++ /dev/null @@ -1 +0,0 @@ -* Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint. diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index f9b80ed1e343..aeb5a838e0c5 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -4,11 +4,11 @@ name: fleet keywords: - fleet - osquery -version: v6.2.2 +version: v6.2.3 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.59.1 +appVersion: v4.60.0 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 7e1c7f7916b2..231c8bb22b79 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -3,7 +3,7 @@ hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy imageRepository: fleetdm/fleet -imageTag: v4.59.1 # Version of Fleet to deploy +imageTag: v4.60.0 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index cd04b77c5028..097ee9befe58 100644 --- a/infrastructure/dogfood/terraform/aws/variables.tf +++ b/infrastructure/dogfood/terraform/aws/variables.tf @@ -56,7 +56,7 @@ variable "database_name" { variable "fleet_image" { description = "the name of the container image to run" - default = "fleetdm/fleet:v4.59.1" + default = "fleetdm/fleet:v4.60.0" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index deb96bc38ec1..4cc4956f107e 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,7 +68,7 @@ variable "redis_mem" { } variable "image" { - default = "fleetdm/fleet:v4.59.1" + default = "fleetdm/fleet:v4.60.0" } variable "software_installers_bucket_name" { diff --git a/infrastructure/guardduty/.terraform.lock.hcl b/infrastructure/guardduty/.terraform.lock.hcl index 5b743eb544e9..f8978d7aa6d4 100644 --- a/infrastructure/guardduty/.terraform.lock.hcl +++ b/infrastructure/guardduty/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.59.1" - constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.1" + version = "4.60.0" + constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.0" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/guardduty/main.tf b/infrastructure/guardduty/main.tf index fdeb7607e00e..f1ce03a2748d 100644 --- a/infrastructure/guardduty/main.tf +++ b/infrastructure/guardduty/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.1" + version = "~> 4.60.0" } } backend "s3" { diff --git a/infrastructure/infrastructure/cloudtrail/main.tf b/infrastructure/infrastructure/cloudtrail/main.tf index 0eaff5aff2ea..a000f06d08ad 100644 --- a/infrastructure/infrastructure/cloudtrail/main.tf +++ b/infrastructure/infrastructure/cloudtrail/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.1" + version = "~> 4.60.0" } } backend "s3" { diff --git a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl index c327efe67542..4ed29230cf87 100644 --- a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl +++ b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.59.1" - constraints = ">= 3.63.0, ~> 4.59.1" + version = "4.60.0" + constraints = ">= 3.63.0, ~> 4.60.0" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/infrastructure/elastic-agent/main.tf b/infrastructure/infrastructure/elastic-agent/main.tf index 78f310682be3..41f8b21f8e92 100644 --- a/infrastructure/infrastructure/elastic-agent/main.tf +++ b/infrastructure/infrastructure/elastic-agent/main.tf @@ -20,7 +20,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.1" + version = "~> 4.60.0" } } backend "s3" { diff --git a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl index 5b743eb544e9..f8978d7aa6d4 100644 --- a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl +++ b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.59.1" - constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.1" + version = "4.60.0" + constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.0" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/infrastructure/guardduty-alerts/main.tf b/infrastructure/infrastructure/guardduty-alerts/main.tf index 4d0e0f4a6805..698cfd3e2250 100644 --- a/infrastructure/infrastructure/guardduty-alerts/main.tf +++ b/infrastructure/infrastructure/guardduty-alerts/main.tf @@ -15,7 +15,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.1" + version = "~> 4.60.0" } } backend "s3" { diff --git a/infrastructure/infrastructure/spend_alerts/main.tf b/infrastructure/infrastructure/spend_alerts/main.tf index 837d69399e1a..7af7ceac5463 100644 --- a/infrastructure/infrastructure/spend_alerts/main.tf +++ b/infrastructure/infrastructure/spend_alerts/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.59.1" + version = "~> 4.60.0" } } backend "s3" { diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf index b372a36ff8d4..bdf2ae161c88 100644 --- a/terraform/addons/vuln-processing/variables.tf +++ b/terraform/addons/vuln-processing/variables.tf @@ -24,7 +24,7 @@ variable "fleet_config" { vuln_processing_cpu = optional(number, 2048) vuln_data_stream_mem = optional(number, 1024) vuln_data_stream_cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.59.1") + image = optional(string, "fleetdm/fleet:v4.60.0") family = optional(string, "fleet-vuln-processing") sidecars = optional(list(any), []) extra_environment_variables = optional(map(string), {}) @@ -82,7 +82,7 @@ variable "fleet_config" { vuln_processing_cpu = 2048 vuln_data_stream_mem = 1024 vuln_data_stream_cpu = 512 - image = "fleetdm/fleet:v4.59.1" + image = "fleetdm/fleet:v4.60.0" family = "fleet-vuln-processing" sidecars = [] extra_environment_variables = {} diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 580e94cbf51d..2ffd63d25a69 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -16,7 +16,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.1") + image = optional(string, "fleetdm/fleet:v4.60.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -119,7 +119,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.1" + image = "fleetdm/fleet:v4.60.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index ddd474e14ba7..94316f6d7a18 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -77,7 +77,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.1") + image = optional(string, "fleetdm/fleet:v4.60.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -205,7 +205,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.1" + image = "fleetdm/fleet:v4.60.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 855ab59f9fc0..bca3bd652c85 100644 --- a/terraform/byo-vpc/example/main.tf +++ b/terraform/byo-vpc/example/main.tf @@ -17,7 +17,7 @@ provider "aws" { } locals { - fleet_image = "fleetdm/fleet:v4.59.1" + fleet_image = "fleetdm/fleet:v4.60.0" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index f85ddb408381..a8ca6742baa7 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -170,7 +170,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.1") + image = optional(string, "fleetdm/fleet:v4.60.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -298,7 +298,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.1" + image = "fleetdm/fleet:v4.60.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/example/main.tf b/terraform/example/main.tf index 7f169df3e89b..4f8b3e035de9 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -63,8 +63,8 @@ module "fleet" { fleet_config = { # To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror - # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.59.1" - image = "fleetdm/fleet:v4.59.1" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.60.0" + image = "fleetdm/fleet:v4.60.0" # override default to deploy the image you desire # See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling # memory and cpu. autoscaling = { diff --git a/terraform/variables.tf b/terraform/variables.tf index 9f08b701df38..8f77be61526c 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -218,7 +218,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.59.1") + image = optional(string, "fleetdm/fleet:v4.60.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -346,7 +346,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.59.1" + image = "fleetdm/fleet:v4.60.0" family = "fleet" sidecars = [] depends_on = [] diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index d9d07156ca18..5dd2236b30d2 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.59.1", + "version": "v4.60.0", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" From 4df0f4e9ea0268381dd0be2808eb2eff493589ca Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Tue, 3 Dec 2024 10:12:07 -0600 Subject: [PATCH 35/36] Fixed gitops issue with gitops role. (#24297) #24288 PR for API docs: https://github.com/fleetdm/fleet/pull/24303 # Checklist for submitter - [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/Committing-Changes.md#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/24288-mdm-gitops-role | 1 + cmd/fleetctl/gitops.go | 4 +-- cmd/fleetctl/gitops_test.go | 12 +++++++ ee/server/service/mdm.go | 16 +++++++++ ee/server/service/mdm_test.go | 46 ++++++++++++++++++++++++ server/datastore/mysql/apple_mdm_test.go | 18 +++++++++- server/fleet/service.go | 3 ++ server/service/apple_mdm.go | 29 +++++++++++++++ server/service/client_mdm.go | 8 ++--- server/service/handler.go | 1 + server/service/integration_mdm_test.go | 7 ++++ 11 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 changes/24288-mdm-gitops-role diff --git a/changes/24288-mdm-gitops-role b/changes/24288-mdm-gitops-role new file mode 100644 index 000000000000..2d04811311b2 --- /dev/null +++ b/changes/24288-mdm-gitops-role @@ -0,0 +1 @@ +Fixed breaking with gitops user role running `fleetctl gitops` command when MDM is enabled. diff --git a/cmd/fleetctl/gitops.go b/cmd/fleetctl/gitops.go index 30efe0a513a7..5389065913ac 100644 --- a/cmd/fleetctl/gitops.go +++ b/cmd/fleetctl/gitops.go @@ -299,12 +299,12 @@ func checkABMTeamAssignments(config *spec.GitOps, fleetClient *service.Client) ( return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage) } - abmToks, err := fleetClient.ListABMTokens() + abmToks, err := fleetClient.CountABMTokens() if err != nil { return nil, false, false, err } - if hasLegacyConfig && len(abmToks) > 1 { + if hasLegacyConfig && abmToks > 1 { return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage) } diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 407351c154b2..72d999823003 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -1217,6 +1217,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { return []*fleet.ABMToken{}, nil } + ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) { + return 0, nil + } ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error { return nil } @@ -1815,6 +1818,9 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) { ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { return []*fleet.ABMToken{}, nil } + ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) { + return 0, nil + } apnsCert, apnsKey, err := mysql.GenerateTestCertBytes() require.NoError(t, err) @@ -2854,6 +2860,9 @@ software: } return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil } + ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) { + return len(tt.tokens), nil + } ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) { var res []*fleet.TeamSummary @@ -3177,6 +3186,9 @@ software: ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil } + ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) { + return 1, nil + } ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) { var res []*fleet.TeamSummary diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 674329d9e493..2de82b708025 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -1274,6 +1274,22 @@ func (svc *Service) ListABMTokens(ctx context.Context) ([]*fleet.ABMToken, error return tokens, nil } +func (svc *Service) CountABMTokens(ctx context.Context) (int, error) { + // Authorizing using the more general AppConfig object because: + // - this service method returns a count, which is not sensitive information + // - gitops role, which needs this info, is not authorized for AppleBM access (as of 2024/12/02) + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { + return 0, err + } + + tokens, err := svc.ds.GetABMTokenCount(ctx) + if err != nil { + return 0, ctxerr.Wrap(ctx, err, "count ABM tokens") + } + + return tokens, nil +} + func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOSTeamID, iOSTeamID, iPadOSTeamID *uint) (*fleet.ABMToken, error) { if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionWrite); err != nil { return nil, err diff --git a/ee/server/service/mdm_test.go b/ee/server/service/mdm_test.go index 21cc3d21d939..5162f20f4dcb 100644 --- a/ee/server/service/mdm_test.go +++ b/ee/server/service/mdm_test.go @@ -6,12 +6,15 @@ import ( "strings" "testing" + "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -148,3 +151,46 @@ b1xn1jGQd/o0xFf9ojpDNy6vNojidQGHh6E3h0GYvxbnQmVNq5U= // prevent static analysis tools from raising issues due to detection of // private key in code. func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +func TestCountABMTokensAuth(t *testing.T) { + t.Parallel() + ds := new(mock.Store) + ctx := context.Background() + authorizer, err := authz.NewAuthorizer() + require.NoError(t, err) + svc := Service{ds: ds, authz: authorizer} + + ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) { + return 5, nil + } + + t.Run("CountABMTokens", func(t *testing.T) { + cases := []struct { + desc string + user *fleet.User + shoudFailWithAuth bool + }{ + {"no role", test.UserNoRoles, true}, + {"gitops can read", test.UserGitOps, false}, + {"maintainer can read", test.UserMaintainer, false}, + {"observer can read", test.UserObserver, false}, + {"observer+ can read", test.UserObserverPlus, false}, + {"admin can read", test.UserAdmin, false}, + {"tm1 gitops cannot read", test.UserTeamGitOpsTeam1, true}, + {"tm1 maintainer can read", test.UserTeamMaintainerTeam1, false}, + {"tm1 observer can read", test.UserTeamObserverTeam1, false}, + {"tm1 observer+ can read", test.UserTeamObserverPlusTeam1, false}, + {"tm1 admin can read", test.UserTeamAdminTeam1, false}, + } + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + ctx = test.UserContext(ctx, c.user) + count, err := svc.CountABMTokens(ctx) + checkAuthErr(t, c.shoudFailWithAuth, err) + if !c.shoudFailWithAuth { + assert.EqualValues(t, 5, count) + } + }) + } + }) +} diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 3cf3e99694c4..83bb3e23c587 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -6545,6 +6545,14 @@ func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) { tm3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team3"}) require.NoError(t, err) + toks, err := ds.ListABMTokens(ctx) + require.NoError(t, err) + require.Empty(t, toks) + + tokCount, err := ds.GetABMTokenCount(ctx) + require.NoError(t, err) + assert.EqualValues(t, 0, tokCount) + // create a token with an empty name and no team set, and another that will be unused encTok := uuid.NewString() @@ -6555,10 +6563,14 @@ func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) { require.NoError(t, err) require.NotEmpty(t, t2.ID) - toks, err := ds.ListABMTokens(ctx) + toks, err = ds.ListABMTokens(ctx) require.NoError(t, err) require.Len(t, toks, 2) + tokCount, err = ds.GetABMTokenCount(ctx) + require.NoError(t, err) + assert.EqualValues(t, 2, tokCount) + // get that token tok, err = ds.GetABMTokenByOrgName(ctx, "") require.NoError(t, err) @@ -6654,6 +6666,10 @@ func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), expTok.MacOSTeam.ID) require.Equal(t, tm2.Name, expTok.IOSTeamName) require.Equal(t, tm3.Name, expTok.IPadOSTeamName) + + tokCount, err = ds.GetABMTokenCount(ctx) + require.NoError(t, err) + assert.EqualValues(t, 1, tokCount) } func testMDMAppleABMTokensTermsExpired(t *testing.T, ds *Datastore) { diff --git a/server/fleet/service.go b/server/fleet/service.go index 7e9f7c973cbb..db7fa3b11040 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -851,6 +851,9 @@ type Service interface { // ListABMTokens lists all the ABM tokens in Fleet. ListABMTokens(ctx context.Context) ([]*ABMToken, error) + // CountABMTokens counts the ABM tokens in Fleet. + CountABMTokens(ctx context.Context) (int, error) + // UpdateABMTokenTeams updates the default macOS, iOS, and iPadOS team IDs for a given ABM token. UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOSTeamID, iOSTeamID, iPadOSTeamID *uint) (*ABMToken, error) diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index c0eb9a7a1f43..da9938611d06 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -4462,6 +4462,35 @@ func (svc *Service) ListABMTokens(ctx context.Context) ([]*fleet.ABMToken, error return nil, fleet.ErrMissingLicense } +// ////////////////////////////////////////////////////////////////////////////// +// Count ABM tokens endpoint +// ////////////////////////////////////////////////////////////////////////////// + +type countABMTokensResponse struct { + Err error `json:"error,omitempty"` + Count int `json:"count"` +} + +func (r countABMTokensResponse) error() error { return r.Err } + +func countABMTokensEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (errorer, error) { + tokenCount, err := svc.CountABMTokens(ctx) + if err != nil { + return &countABMTokensResponse{Err: err}, nil + } + + return &countABMTokensResponse{Count: tokenCount}, nil +} + +func (svc *Service) CountABMTokens(ctx context.Context) (int, error) { + // Automatic enrollment (ABM/ADE/DEP) is a feature that requires a license. + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return 0, fleet.ErrMissingLicense +} + //////////////////////////////////////////////////////////////////////////////// // Update ABM token teams endpoint //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go index c41915b91a01..e849eb4b0fd1 100644 --- a/server/service/client_mdm.go +++ b/server/service/client_mdm.go @@ -40,11 +40,11 @@ func (c *Client) GetAppleBM() (*fleet.AppleBM, error) { return responseBody.AppleBM, err } -func (c *Client) ListABMTokens() ([]*fleet.ABMToken, error) { - verb, path := "GET", "/api/latest/fleet/abm_tokens" - var responseBody listABMTokensResponse +func (c *Client) CountABMTokens() (int, error) { + verb, path := "GET", "/api/latest/fleet/abm_tokens/count" + var responseBody countABMTokensResponse err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "") - return responseBody.Tokens, err + return responseBody.Count, err } // RequestAppleCSR requests a signed CSR from the Fleet server and returns the diff --git a/server/service/handler.go b/server/service/handler.go index 4dc5a467b812..5228e901d8a2 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -750,6 +750,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.POST("/api/_version_/fleet/abm_tokens", uploadABMTokenEndpoint, uploadABMTokenRequest{}) ue.DELETE("/api/_version_/fleet/abm_tokens/{id:[0-9]+}", deleteABMTokenEndpoint, deleteABMTokenRequest{}) ue.GET("/api/_version_/fleet/abm_tokens", listABMTokensEndpoint, nil) + ue.GET("/api/_version_/fleet/abm_tokens/count", countABMTokensEndpoint, nil) ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/teams", updateABMTokenTeamsEndpoint, updateABMTokenTeamsRequest{}) ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/renew", renewABMTokenEndpoint, renewABMTokenRequest{}) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index c0177ab049b2..4c2cd416afd4 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -829,6 +829,10 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() { require.Equal(t, "Fleet", mdmResp.CommonName) require.NotZero(t, mdmResp.RenewDate) + var countTokensResp countABMTokensResponse + s.DoJSON("GET", "/api/latest/fleet/abm_tokens/count", nil, http.StatusOK, &countTokensResp) + assert.EqualValues(t, 0, countTokensResp.Count) + // set up multiple ABM tokens with different org names defaultOrgName := "fleet_test" s.enableABM(defaultOrgName) @@ -860,6 +864,9 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() { require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeam.Name) require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeam.Name) + s.DoJSON("GET", "/api/latest/fleet/abm_tokens/count", nil, http.StatusOK, &countTokensResp) + assert.EqualValues(t, 2, countTokensResp.Count) + // create a new team tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{ Name: t.Name(), From a7401f8767a0cdda33c7692838c59b5284dc0675 Mon Sep 17 00:00:00 2001 From: George Karr Date: Tue, 3 Dec 2024 12:59:36 -0600 Subject: [PATCH 36/36] Adding changes for Fleet v4.60.1 --- CHANGELOG.md | 6 ++++++ changes/24024-bypass-setup-experience-if-empty | 2 -- changes/24288-mdm-gitops-role | 1 - charts/fleet/Chart.yaml | 2 +- charts/fleet/values.yaml | 2 +- infrastructure/dogfood/terraform/aws/variables.tf | 2 +- infrastructure/dogfood/terraform/gcp/variables.tf | 2 +- infrastructure/guardduty/.terraform.lock.hcl | 4 ++-- infrastructure/guardduty/main.tf | 2 +- infrastructure/infrastructure/cloudtrail/main.tf | 2 +- .../infrastructure/elastic-agent/.terraform.lock.hcl | 4 ++-- infrastructure/infrastructure/elastic-agent/main.tf | 2 +- .../infrastructure/guardduty-alerts/.terraform.lock.hcl | 4 ++-- infrastructure/infrastructure/guardduty-alerts/main.tf | 2 +- infrastructure/infrastructure/spend_alerts/main.tf | 2 +- terraform/addons/ses/README.md | 2 +- terraform/addons/vuln-processing/variables.tf | 4 ++-- terraform/byo-vpc/byo-db/README.md | 2 +- terraform/byo-vpc/byo-db/byo-ecs/variables.tf | 4 ++-- terraform/byo-vpc/byo-db/variables.tf | 4 ++-- terraform/byo-vpc/example/main.tf | 2 +- terraform/byo-vpc/variables.tf | 4 ++-- terraform/example/main.tf | 4 ++-- terraform/variables.tf | 4 ++-- tools/fleetctl-npm/package.json | 2 +- 25 files changed, 37 insertions(+), 34 deletions(-) delete mode 100644 changes/24024-bypass-setup-experience-if-empty delete mode 100644 changes/24288-mdm-gitops-role diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3831e84a0a..007448c1c8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Fleet 4.60.1 (Dec 03, 2024) + +### Bug fixes + +- Fixed a bug where breaking occurred with gitops user role running `fleetctl gitops` command when MDM was enabled. + ## Fleet 4.60.0 (Nov 27, 2024) ### Endpoint operations diff --git a/changes/24024-bypass-setup-experience-if-empty b/changes/24024-bypass-setup-experience-if-empty deleted file mode 100644 index 319df88c1c91..000000000000 --- a/changes/24024-bypass-setup-experience-if-empty +++ /dev/null @@ -1,2 +0,0 @@ -* Bypass the setup experience UI if there is no setup experience item to process (no software to install, no script to execute), so that releasing the device is done without going through that window. -* Fixed releasing a DEP-enrolled macOS device if mTLS is configured for `fleetd`. diff --git a/changes/24288-mdm-gitops-role b/changes/24288-mdm-gitops-role deleted file mode 100644 index 2d04811311b2..000000000000 --- a/changes/24288-mdm-gitops-role +++ /dev/null @@ -1 +0,0 @@ -Fixed breaking with gitops user role running `fleetctl gitops` command when MDM is enabled. diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index aeb5a838e0c5..d8da013055b0 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,7 +8,7 @@ version: v6.2.3 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.60.0 +appVersion: v4.60.1 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 231c8bb22b79..e8570a7d600a 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -3,7 +3,7 @@ hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy imageRepository: fleetdm/fleet -imageTag: v4.60.0 # Version of Fleet to deploy +imageTag: v4.60.1 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index 097ee9befe58..896e7474e727 100644 --- a/infrastructure/dogfood/terraform/aws/variables.tf +++ b/infrastructure/dogfood/terraform/aws/variables.tf @@ -56,7 +56,7 @@ variable "database_name" { variable "fleet_image" { description = "the name of the container image to run" - default = "fleetdm/fleet:v4.60.0" + default = "fleetdm/fleet:v4.60.1" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index 4cc4956f107e..eb0391b392d4 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,7 +68,7 @@ variable "redis_mem" { } variable "image" { - default = "fleetdm/fleet:v4.60.0" + default = "fleetdm/fleet:v4.60.1" } variable "software_installers_bucket_name" { diff --git a/infrastructure/guardduty/.terraform.lock.hcl b/infrastructure/guardduty/.terraform.lock.hcl index f8978d7aa6d4..1f3b9a6b8471 100644 --- a/infrastructure/guardduty/.terraform.lock.hcl +++ b/infrastructure/guardduty/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.60.0" - constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.0" + version = "4.60.1" + constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.1" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/guardduty/main.tf b/infrastructure/guardduty/main.tf index f1ce03a2748d..a68123626f23 100644 --- a/infrastructure/guardduty/main.tf +++ b/infrastructure/guardduty/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.60.0" + version = "~> 4.60.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/cloudtrail/main.tf b/infrastructure/infrastructure/cloudtrail/main.tf index a000f06d08ad..a8232723e7c5 100644 --- a/infrastructure/infrastructure/cloudtrail/main.tf +++ b/infrastructure/infrastructure/cloudtrail/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.60.0" + version = "~> 4.60.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl index 4ed29230cf87..3bf60fe7cc31 100644 --- a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl +++ b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.60.0" - constraints = ">= 3.63.0, ~> 4.60.0" + version = "4.60.1" + constraints = ">= 3.63.0, ~> 4.60.1" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/infrastructure/elastic-agent/main.tf b/infrastructure/infrastructure/elastic-agent/main.tf index 41f8b21f8e92..383fd562cfb2 100644 --- a/infrastructure/infrastructure/elastic-agent/main.tf +++ b/infrastructure/infrastructure/elastic-agent/main.tf @@ -20,7 +20,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.60.0" + version = "~> 4.60.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl index f8978d7aa6d4..1f3b9a6b8471 100644 --- a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl +++ b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl @@ -2,8 +2,8 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.60.0" - constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.0" + version = "4.60.1" + constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.1" hashes = [ "h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=", "zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4", diff --git a/infrastructure/infrastructure/guardduty-alerts/main.tf b/infrastructure/infrastructure/guardduty-alerts/main.tf index 698cfd3e2250..d39ad9e1817b 100644 --- a/infrastructure/infrastructure/guardduty-alerts/main.tf +++ b/infrastructure/infrastructure/guardduty-alerts/main.tf @@ -15,7 +15,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.60.0" + version = "~> 4.60.1" } } backend "s3" { diff --git a/infrastructure/infrastructure/spend_alerts/main.tf b/infrastructure/infrastructure/spend_alerts/main.tf index 7af7ceac5463..203822163486 100644 --- a/infrastructure/infrastructure/spend_alerts/main.tf +++ b/infrastructure/infrastructure/spend_alerts/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.60.0" + version = "~> 4.60.1" } } backend "s3" { diff --git a/terraform/addons/ses/README.md b/terraform/addons/ses/README.md index 7b549d31ccf7..bf74e252eabf 100644 --- a/terraform/addons/ses/README.md +++ b/terraform/addons/ses/README.md @@ -9,7 +9,7 @@ No requirements. | Name | Version | |------|---------| -| [aws](#provider\_aws) | 4.60.0 | +| [aws](#provider\_aws) | 4.60.1 | ## Modules diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf index bdf2ae161c88..b1c9aec6bd79 100644 --- a/terraform/addons/vuln-processing/variables.tf +++ b/terraform/addons/vuln-processing/variables.tf @@ -24,7 +24,7 @@ variable "fleet_config" { vuln_processing_cpu = optional(number, 2048) vuln_data_stream_mem = optional(number, 1024) vuln_data_stream_cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.60.0") + image = optional(string, "fleetdm/fleet:v4.60.1") family = optional(string, "fleet-vuln-processing") sidecars = optional(list(any), []) extra_environment_variables = optional(map(string), {}) @@ -82,7 +82,7 @@ variable "fleet_config" { vuln_processing_cpu = 2048 vuln_data_stream_mem = 1024 vuln_data_stream_cpu = 512 - image = "fleetdm/fleet:v4.60.0" + image = "fleetdm/fleet:v4.60.1" family = "fleet-vuln-processing" sidecars = [] extra_environment_variables = {} diff --git a/terraform/byo-vpc/byo-db/README.md b/terraform/byo-vpc/byo-db/README.md index 60d144448934..3783008815e2 100644 --- a/terraform/byo-vpc/byo-db/README.md +++ b/terraform/byo-vpc/byo-db/README.md @@ -6,7 +6,7 @@ No requirements. | Name | Version | |------|---------| -| [aws](#provider\_aws) | 4.60.0 | +| [aws](#provider\_aws) | 4.60.1 | ## Modules diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 2ffd63d25a69..ddebead667f6 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -16,7 +16,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.60.0") + image = optional(string, "fleetdm/fleet:v4.60.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -119,7 +119,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.60.0" + image = "fleetdm/fleet:v4.60.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index 94316f6d7a18..476f6f755837 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -77,7 +77,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.60.0") + image = optional(string, "fleetdm/fleet:v4.60.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -205,7 +205,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.60.0" + image = "fleetdm/fleet:v4.60.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index bca3bd652c85..4acb7e7be87d 100644 --- a/terraform/byo-vpc/example/main.tf +++ b/terraform/byo-vpc/example/main.tf @@ -17,7 +17,7 @@ provider "aws" { } locals { - fleet_image = "fleetdm/fleet:v4.60.0" + fleet_image = "fleetdm/fleet:v4.60.1" domain_name = "example.com" } diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index a8ca6742baa7..e1684a63a425 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -170,7 +170,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.60.0") + image = optional(string, "fleetdm/fleet:v4.60.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -298,7 +298,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.60.0" + image = "fleetdm/fleet:v4.60.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/terraform/example/main.tf b/terraform/example/main.tf index 4f8b3e035de9..b0922c2906d3 100644 --- a/terraform/example/main.tf +++ b/terraform/example/main.tf @@ -63,8 +63,8 @@ module "fleet" { fleet_config = { # To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror - # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.60.0" - image = "fleetdm/fleet:v4.60.0" # override default to deploy the image you desire + # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.60.1" + image = "fleetdm/fleet:v4.60.1" # override default to deploy the image you desire # See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling # memory and cpu. autoscaling = { diff --git a/terraform/variables.tf b/terraform/variables.tf index 8f77be61526c..892ba29be866 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -218,7 +218,7 @@ variable "fleet_config" { mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) - image = optional(string, "fleetdm/fleet:v4.60.0") + image = optional(string, "fleetdm/fleet:v4.60.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) @@ -346,7 +346,7 @@ variable "fleet_config" { mem = 512 cpu = 256 pid_mode = null - image = "fleetdm/fleet:v4.60.0" + image = "fleetdm/fleet:v4.60.1" family = "fleet" sidecars = [] depends_on = [] diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index 5dd2236b30d2..9ca552068b9d 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.60.0", + "version": "v4.60.1", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js"