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)