From 39f10293901206176ef346104fe51cd85c687bb0 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 23 Dec 2022 13:05:16 -0300 Subject: [PATCH] Make activities typed and auto-generate docs for each type (#9069) * Make activities type and auto-generate docs for each type * Add pageOrderInSection to not break site * Add do not edit note to generated file * Add make generate-doc step * Fix main merge --- Makefile | 5 +- cmd/fleetctl/apply_test.go | 14 +- cmd/fleetctl/delete_test.go | 4 +- cmd/fleetctl/query_test.go | 2 +- cmd/fleetctl/users_test.go | 11 +- docs/Using-Fleet/Audit-Activities.md | 512 ++++++++++++++ ee/server/service/teams.go | 34 +- ee/server/service/users.go | 3 +- server/datastore/mysql/activities.go | 6 +- server/datastore/mysql/activities_test.go | 42 +- server/fleet/activities.go | 649 ++++++++++++++++-- server/fleet/datastore.go | 2 +- server/fleet/gen_activity_doc.go | 73 ++ server/mock/datastore_mock.go | 6 +- server/service/activities.go | 36 +- server/service/activities_test.go | 4 +- server/service/appconfig.go | 5 +- server/service/campaigns.go | 11 +- server/service/campaigns_test.go | 12 +- server/service/global_policies.go | 17 +- server/service/global_policies_test.go | 2 +- server/service/http_auth_test.go | 2 +- server/service/integration_core_test.go | 19 +- server/service/integration_enterprise_test.go | 8 +- server/service/integration_sso_test.go | 2 +- server/service/osquery_test.go | 10 +- server/service/packs.go | 25 +- server/service/packs_test.go | 2 +- server/service/queries.go | 32 +- server/service/queries_test.go | 2 +- server/service/service_campaign_test.go | 2 +- server/service/service_users.go | 7 +- server/service/sessions.go | 9 +- server/service/team_policies.go | 18 +- server/service/team_policies_test.go | 2 +- server/service/teams_test.go | 12 +- server/service/users.go | 7 +- server/service/users_test.go | 2 +- 38 files changed, 1412 insertions(+), 199 deletions(-) create mode 100644 docs/Using-Fleet/Audit-Activities.md create mode 100644 server/fleet/gen_activity_doc.go diff --git a/Makefile b/Makefile index dfa246424b..7f12402d00 100644 --- a/Makefile +++ b/Makefile @@ -176,6 +176,9 @@ generate-mock: .prefix go install github.com/groob/mockimpl@latest go generate github.com/fleetdm/fleet/v4/server/mock github.com/fleetdm/fleet/v4/server/mock/mockresult +generate-doc: .prefix + go generate github.com/fleetdm/fleet/v4/server/fleet + deps: deps-js deps-go deps-js: @@ -400,4 +403,4 @@ db-replica-reset: fleet # db-replica-run runs fleet serve with one main and one read MySQL instance. db-replica-run: fleet - FLEET_MYSQL_ADDRESS=127.0.0.1:3308 FLEET_MYSQL_READ_REPLICA_ADDRESS=127.0.0.1:3309 FLEET_MYSQL_READ_REPLICA_USERNAME=fleet FLEET_MYSQL_READ_REPLICA_DATABASE=fleet FLEET_MYSQL_READ_REPLICA_PASSWORD=insecure ./build/fleet serve --dev --dev_license + FLEET_MYSQL_ADDRESS=127.0.0.1:3308 FLEET_MYSQL_READ_REPLICA_ADDRESS=127.0.0.1:3309 FLEET_MYSQL_READ_REPLICA_USERNAME=fleet FLEET_MYSQL_READ_REPLICA_DATABASE=fleet FLEET_MYSQL_READ_REPLICA_PASSWORD=insecure ./build/fleet serve --dev --dev_license \ No newline at end of file diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index affb3203d6..261d4aa558 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -145,7 +145,7 @@ func TestApplyTeamSpecs(t *testing.T) { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -241,7 +241,7 @@ func TestApplyAppConfig(t *testing.T) { return userRoleSpecList, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -316,7 +316,7 @@ func TestApplyAppConfigDryRunIssue(t *testing.T) { return userRoleSpecList[1], nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -507,7 +507,7 @@ func TestApplyPolicies(t *testing.T) { } return nil, errors.New("unexpected team name!") } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -609,7 +609,7 @@ func TestApplyPacks(t *testing.T) { ds.ListPacksFunc = func(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) { return nil, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -672,7 +672,7 @@ func TestApplyQueries(t *testing.T) { appliedQueries = queries return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -809,7 +809,7 @@ func TestApplySpecs(t *testing.T) { } // activities - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } diff --git a/cmd/fleetctl/delete_test.go b/cmd/fleetctl/delete_test.go index 5fcc5a4dce..ec0963ef6e 100644 --- a/cmd/fleetctl/delete_test.go +++ b/cmd/fleetctl/delete_test.go @@ -52,7 +52,7 @@ func TestDeletePack(t *testing.T) { Disabled: false, }, true, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -95,7 +95,7 @@ func TestDeleteQuery(t *testing.T) { ObserverCanRun: false, }, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } diff --git a/cmd/fleetctl/query_test.go b/cmd/fleetctl/query_test.go index 49913b224c..1184fb5169 100644 --- a/cmd/fleetctl/query_test.go +++ b/cmd/fleetctl/query_test.go @@ -64,7 +64,7 @@ func TestLiveQuery(t *testing.T) { ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) { return fleet.TargetMetrics{TotalHosts: 1, OnlineHosts: 1}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } diff --git a/cmd/fleetctl/users_test.go b/cmd/fleetctl/users_test.go index d7d6bf659c..5767ec4ac3 100644 --- a/cmd/fleetctl/users_test.go +++ b/cmd/fleetctl/users_test.go @@ -34,8 +34,8 @@ func TestUserDelete(t *testing.T) { deletedUser = id return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { - assert.Equal(t, fleet.ActivityTypeDeletedUser, activityType) + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + assert.Equal(t, fleet.ActivityTypeDeletedUser{}.ActivityName(), activity.ActivityName()) return nil } @@ -66,7 +66,7 @@ func TestUserCreateForcePasswordReset(t *testing.T) { ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) { return nil, ¬FoundError{} } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -124,7 +124,7 @@ func TestCreateBulkUsers(t *testing.T) { ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) { return nil, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } @@ -142,12 +142,11 @@ func TestCreateBulkUsers(t *testing.T) { assert.Equal(t, "", runAppForTest(t, []string{"user", "create-users", "--csv", csvFile})) assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "user_roles", "--json"})) - } func TestDeleteBulkUsers(t *testing.T) { _, ds := runServerWithMockedDS(t) - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } csvFilePath := writeTmpCsv(t, diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md new file mode 100644 index 0000000000..4ab1415274 --- /dev/null +++ b/docs/Using-Fleet/Audit-Activities.md @@ -0,0 +1,512 @@ + +# Audit Activities + +Fleet logs the following information for administrative actions (in JSON): + +- `created_at`: Timestamp of the event. +- `id`: Unique ID of the generated event in Fleet. +- `actor_full_name`: Author user name (missing if the user was deleted). +- `actor_id`: Unique ID of the author in Fleet (missing if the user was deleted). +- `actor_gravatar`: Gravatar URL of the author (missing if the user was deleted). +- `actor_email`: E-mail of the author (missing if the user was deleted). +- `type`: Type of the activity (see all types below). +- `details`: Specific details depending on the type of activity (see details for each activity type below). + +Example: +```json +{ + "created_at": "2022-12-20T14:54:17Z", + "id": 6, + "actor_full_name": "Gandalf", + "actor_id": 2, + "actor_gravatar": "foo@example.com", + "actor_email": "foo@example.com", + "type": "edited_saved_query", + "details":{ + "query_id": 42, + "query_name": "Some query name" + } +} +``` + +## List of activities and their specific details + +### Type `created_pack` + +Generated when creating scheduled query packs. + +This activity contains the following fields: +- "pack_id": the id of the created pack. +- "pack_name": the name of the created pack. + +#### Example + +```json +{ + "pack_id": 123, + "pack_name": "foo" +} +``` + +### Type `edited_pack` + +Generated when editing scheduled query packs. + +This activity contains the following fields: +- "pack_id": the id of the edited pack. +- "pack_name": the name of the edited pack. + +#### Example + +```json +{ + "pack_id": 123, + "pack_name": "foo" +} +``` + +### Type `deleted_pack` + +Generated when deleting scheduled query packs. + +This activity contains the following fields: +- "pack_name": the name of the created pack. + +#### Example + +```json +{ + "pack_name": "foo" +} +``` + +### Type `applied_spec_pack` + +Generated when applying a scheduled query pack spec. + +This activity does not contain any detail fields. + +### Type `created_policy` + +Generated when creating policies. + +This activity contains the following fields: +- "policy_id": the ID of the created policy. +- "policy_name": the name of the created policy. + +#### Example + +```json +{ + "policy_id": 123, + "policy_name": "foo" +} +``` + +### Type `edited_policy` + +Generated when editing policies. + +This activity contains the following fields: +- "policy_id": the ID of the edited policy. +- "policy_name": the name of the edited policy. + +#### Example + +```json +{ + "policy_id": 123, + "policy_name": "foo" +} +``` + +### Type `deleted_policy` + +Generated when deleting policies. + +This activity contains the following fields: +- "policy_id": the ID of the deleted policy. +- "policy_name": the name of the deleted policy. + +#### Example + +```json +{ + "policy_id": 123, + "policy_name": "foo" +} +``` + +### Type `applied_spec_policy` + +Generated when applying policy specs. + +This activity contains a field "policies" where each item is a policy spec with the following fields: +- "name": Name of the applied policy. +- "query": SQL query of the policy. +- "description": Description of the policy. +- "critical": Marks the policy as high impact. +- "resolution": Describes how to solve a failing policy. +- "team": Name of the team this policy belongs to. +- "platform": Comma-separated string to indicate the target platforms. + + +#### Example + +```json +{ + "policies": [ + { + "name":"Gatekeeper enabled (macOS)", + "query":"SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;", + "critical":false, + "platform":"darwin", + "resolution":"To enable Gatekeeper, on the failing device [...]", + "description":"Checks to make sure that the Gatekeeper feature is [...]" + }, + { + "name":"Full disk encryption enabled (Windows)", + "query":"SELECT 1 FROM bitlocker_info WHERE drive_letter='C:' AND protection_status=1;", + "critical":false, + "platform":"windows", + "resolution":"To get additional information, run the following osquery [...]", + "description":"Checks to make sure that full disk encryption is enabled on Windows devices." + } + ] +} +``` + +### Type `created_saved_query` + +Generated when creating a new query. + +This activity contains the following fields: +- "query_id": the ID of the created query. +- "query_name": the name of the created query. + +#### Example + +```json +{ + "query_id": 123, + "query_name": "foo" +} +``` + +### Type `edited_saved_query` + +Generated when editing a saved query. + +This activity contains the following fields: +- "query_id": the ID of the query being edited. +- "query_name": the name of the query being edited. + +#### Example + +```json +{ + "query_id": 123, + "query_name": "foo" +} +``` + +### Type `deleted_saved_query` + +Generated when deleting a saved query. + +This activity contains the following fields: +- "query_name": the name of the query being deleted. + +#### Example + +```json +{ + "query_name": "foo" +} +``` + +### Type `deleted_multiple_saved_query` + +Generated when deleting multiple saved queries. + +This activity contains the following fields: +- "query_ids": list of IDs of the deleted saved queries. + +#### Example + +```json +{ + "query_ids": [1, 42, 100] +} +``` + +### Type `applied_spec_saved_query` + +Generated when applying a query spec. + +This activity contains a field "specs" where each item is a query spec with the following fields: +- "name": Name of the query. +- "description": Description of the query. +- "query": SQL query. + +#### Example + +```json +{ + "specs": [ + { + "name":"Get OpenSSL versions", + "query":"SELECT name AS name, version AS version, 'deb_packages' AS source FROM [...]", + "description":"Retrieves the OpenSSL version." + } + ] +} +``` + +### Type `created_team` + +Generated when creating teams. + +This activity contains the following fields: +- "team_id": unique ID of the created team. +- "team_name": the name of the created team. + +#### Example + +```json +{ + "team_id": 123, + "team_name": "foo" +} +``` + +### Type `deleted_team` + +Generated when deleting teams. + +This activity contains the following fields: +- "team_id": unique ID of the deleted team. +- "team_name": the name of the deleted team. + +#### Example + +```json +{ + "team_id": 123, + "team_name": "foo" +} +``` + +### Type `applied_spec_team` + +Generated when applying team specs. + +This activity contains a field "teams" where each item contains the team details with the following fields: +- "id": Unique ID of the team. +- "name": Name of the team. + +#### Example + +```json +{ + "teams": [ + { + "id": 123, + "name": "foo" + } + ] +} +``` + +### Type `edited_agent_options` + +Generated when agent options are edited (either globally or for a team). + +This activity contains the following fields: +- "global": "true" if the user updated the global agent options, "false" if the agent options of a team were updated. +- "team_id": unique ID of the team for which the agent options were updated (null if global is true). +- "team_name": the name of the team for which the agent options were updated (null if global is true). + +#### Example + +```json +{ + "team_id": 123, + "team_name": "foo", + "global": false +} +``` + +### Type `live_query` + +Generated when running live queries. + +This activity contains the following fields: +- "targets_count": Number of hosts where the live query was targeted to run. +- "query_sql": The SQL query to run on hosts. +- "query_name": Name of the query (this field is not set if this was not a saved query). + +#### Example + +```json +{ + "targets_count": 5000, + "query_sql": "SELECT * from osquery_info;", + "query_name": "foo" +} +``` + +### Type `user_added_by_sso` + +Generated when new users are added via SSO JIT provisioning + +This activity does not contain any detail fields. + +### Type `user_logged_in` + +Generated when users successfully log in to Fleet. + +This activity contains the following fields: +- "public_ip": Public IP of the login request. + +#### Example + +```json +{ + "public_ip": "168.226.215.82" +} +``` + +### Type `created_user` + +Generated when a user is created. + +This activity contains the following fields: +- "user_id": Unique ID of the created user in Fleet. +- "user_name": Name of the created user. +- "user_email": E-mail of the created user. + +#### Example + +```json +{ + "user_id": 42, + "user_name": "Foo", + "user_email": "foo@example.com" +} +``` + +### Type `deleted_user` + +Generated when a user is deleted. + +This activity contains the following fields: +- "user_id": Unique ID of the deleted user in Fleet. +- "user_name": Name of the deleted user. +- "user_email": E-mail of the deleted user. + +#### Example + +```json +{ + "user_id": 42, + "user_name": "Foo", + "user_email": "foo@example.com" +} +``` + +### Type `changed_user_global_role` + +Generated when user global roles are changed. + +This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": New global role of the edited user. + +#### Example + +```json +{ + "user_id": 42, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Observer" +} +``` + +### Type `deleted_user_global_role` + +Generated when user global roles are deleted. + +This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": Deleted global role of the edited user. + +#### Example + +```json +{ + "user_id": 43, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Maintainer" +} +``` + +### Type `changed_user_team_role` + +Generated when user team roles are changed. + +This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": Team role set to the edited user. +- "team_id": Unique ID of the team of the changed role. +- "team_name": Name of the team of the changed role. + +#### Example + +```json +{ + "user_id": 43, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Maintainer", + "team_id": 5, + "team_name": "Bar" +} +``` + +### Type `deleted_user_team_role` + +Generated when user team roles are deleted. + +This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": Team role deleted from the edited user. +- "team_id": Unique ID of the team of the deleted role. +- "team_name": Name of the team of the deleted role. + +#### Example + +```json +{ + "user_id": 44, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Observer", + "team_id": 2, + "team_name": "Zoo" +} +``` + + + + \ No newline at end of file diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 7567782432..c5019208fd 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -69,8 +69,10 @@ func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Te if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeCreatedTeam, - &map[string]interface{}{"team_id": team.ID, "team_name": team.Name}, + fleet.ActivityTypeCreatedTeam{ + ID: team.ID, + Name: team.Name, + }, ); err != nil { return nil, err } @@ -177,8 +179,11 @@ func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, teamID uint, tea if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeEditedAgentOptions, - &map[string]interface{}{"global": false, "team_id": team.ID, "team_name": team.Name}, + fleet.ActivityTypeEditedAgentOptions{ + Global: false, + TeamID: &team.ID, + TeamName: &team.Name, + }, ); err != nil { return nil, ctxerr.Wrap(ctx, err, "create edited agent options activity") } @@ -330,8 +335,10 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error { return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedTeam, - &map[string]interface{}{"team_id": teamID, "team_name": name}, + fleet.ActivityTypeDeletedTeam{ + ID: teamID, + Name: name, + }, ) } @@ -410,11 +417,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, return err } - type activityDetail struct { - ID uint `json:"id"` - Name string `json:"name"` - } - var details []activityDetail + var details []fleet.TeamActivityDetail for _, spec := range specs { var secrets []*fleet.EnrollSecret @@ -462,7 +465,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, if err != nil { return ctxerr.Wrap(ctx, err, "creating team from spec") } - details = append(details, activityDetail{ + details = append(details, fleet.TeamActivityDetail{ ID: team.ID, Name: team.Name, }) @@ -473,7 +476,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, return ctxerr.Wrap(ctx, err, "editing team from spec") } - details = append(details, activityDetail{ + details = append(details, fleet.TeamActivityDetail{ ID: team.ID, Name: team.Name, }) @@ -483,8 +486,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeAppliedSpecTeam, - &map[string]interface{}{"teams": details}, + fleet.ActivityTypeAppliedSpecTeam{ + Teams: details, + }, ); err != nil { return ctxerr.Wrap(ctx, err, "create applied team spec activity") } diff --git a/ee/server/service/users.go b/ee/server/service/users.go index 4412566cbd..36259a900d 100644 --- a/ee/server/service/users.go +++ b/ee/server/service/users.go @@ -55,8 +55,7 @@ func (svc *Service) GetSSOUser(ctx context.Context, auth fleet.Auth) (*fleet.Use err = svc.ds.NewActivity( ctx, user, - fleet.ActivityTypeUserAddedBySSO, - &map[string]interface{}{}, + fleet.ActivityTypeUserAddedBySSO{}, ) if err != nil { return nil, err diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index e640cf8fa5..f7a9549fdc 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -11,8 +11,8 @@ import ( ) // NewActivity stores an activity item that the user performed -func (ds *Datastore) NewActivity(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { - detailsBytes, err := json.Marshal(details) +func (ds *Datastore) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + detailsBytes, err := json.Marshal(activity) if err != nil { return ctxerr.Wrap(ctx, err, "marshaling activity details") } @@ -20,7 +20,7 @@ func (ds *Datastore) NewActivity(ctx context.Context, user *fleet.User, activity `INSERT INTO activities (user_id, user_name, activity_type, details) VALUES(?,?,?,?)`, user.ID, user.Name, - activityType, + activity.ActivityName(), detailsBytes, ) if err != nil { diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go index af97ca730f..42899789b3 100644 --- a/server/datastore/mysql/activities_test.go +++ b/server/datastore/mysql/activities_test.go @@ -2,6 +2,7 @@ package mysql import ( "context" + "encoding/json" "testing" "github.com/fleetdm/fleet/v4/server/fleet" @@ -28,6 +29,27 @@ func TestActivity(t *testing.T) { } } +type dummyActivity struct { + name string `json:"-"` + details map[string]interface{} +} + +func (d dummyActivity) MarshalJSON() ([]byte, error) { + b, err := json.Marshal(d.details) + if err != nil { + return nil, err + } + return b, nil +} + +func (d dummyActivity) ActivityName() string { + return d.name +} + +func (d dummyActivity) Documentation() (activity string, details string, detailsExample string) { + return "", "", "" +} + func testActivityUsernameChange(t *testing.T, ds *Datastore) { u := &fleet.User{ Password: []byte("asd"), @@ -38,8 +60,14 @@ func testActivityUsernameChange(t *testing.T, ds *Datastore) { } _, err := ds.NewUser(context.Background(), u) require.Nil(t, err) - require.NoError(t, ds.NewActivity(context.Background(), u, "test1", &map[string]interface{}{"detail": 1, "sometext": "aaa"})) - require.NoError(t, ds.NewActivity(context.Background(), u, "test2", &map[string]interface{}{"detail": 2})) + require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ + name: "test1", + details: map[string]interface{}{"detail": 1, "sometext": "aaa"}, + })) + require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ + name: "test2", + details: map[string]interface{}{"detail": 2}, + })) activities, err := ds.ListActivities(context.Background(), fleet.ListOptions{}) require.NoError(t, err) @@ -76,8 +104,14 @@ func testActivityNew(t *testing.T, ds *Datastore) { } _, err := ds.NewUser(context.Background(), u) require.Nil(t, err) - require.NoError(t, ds.NewActivity(context.Background(), u, "test1", &map[string]interface{}{"detail": 1, "sometext": "aaa"})) - require.NoError(t, ds.NewActivity(context.Background(), u, "test2", &map[string]interface{}{"detail": 2})) + require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ + name: "test1", + details: map[string]interface{}{"detail": 1, "sometext": "aaa"}, + })) + require.NoError(t, ds.NewActivity(context.Background(), u, dummyActivity{ + name: "test2", + details: map[string]interface{}{"detail": 2}, + })) opt := fleet.ListOptions{ Page: 0, diff --git a/server/fleet/activities.go b/server/fleet/activities.go index f6c8a38596..d4c3ec8ead 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -4,62 +4,427 @@ import ( "encoding/json" ) -const ( - // ActivityTypeUserLoggedIn is the activity type for logging in a user - ActivityTypeUserLoggedIn = "user_logged_in" - // ActivityTypeCreatedPack is the activity type for created packs - ActivityTypeCreatedPack = "created_pack" - // ActivityTypeEditedPack is the activity type for edited packs - ActivityTypeEditedPack = "edited_pack" - // ActivityTypeDeletedPack is the activity type for deleted packs - ActivityTypeDeletedPack = "deleted_pack" - // ActivityTypeAppliedSpecPack is the activity type for pack specs applied - ActivityTypeAppliedSpecPack = "applied_spec_pack" - // ActivityTypeCreatedPolicy is the activity type for created policies - ActivityTypeCreatedPolicy = "created_policy" - // ActivityTypeEditedPolicy is the activity type for edited policies - ActivityTypeEditedPolicy = "edited_policy" - // ActivityTypeDeletedPolicy is the activity type for deleted policies - ActivityTypeDeletedPolicy = "deleted_policy" - // ActivityTypeAppliedSpecPolicy is the activity type for saved queries spec applied - ActivityTypeAppliedSpecPolicy = "applied_spec_policy" - // ActivityTypeCreatedSavedQuery is the activity type for created saved queries - ActivityTypeCreatedSavedQuery = "created_saved_query" - // ActivityTypeEditedSavedQuery is the activity type for edited saved queries - ActivityTypeEditedSavedQuery = "edited_saved_query" - // ActivityTypeDeletedSavedQuery is the activity type for deleted saved queries - ActivityTypeDeletedSavedQuery = "deleted_saved_query" - // ActivityTypeDeletedMultipleSavedQuery is the activity type for multiple deleted saved queries - ActivityTypeDeletedMultipleSavedQuery = "deleted_multiple_saved_query" - // ActivityTypeAppliedSpecSavedQuery is the activity type for saved queries spec applied - ActivityTypeAppliedSpecSavedQuery = "applied_spec_saved_query" - // ActivityTypeCreatedTeam is the activity type for created team - ActivityTypeCreatedTeam = "created_team" - // ActivityTypeDeletedTeam is the activity type for deleted team - ActivityTypeDeletedTeam = "deleted_team" - // ActivityTypeLiveQuery is the activity type for live queries - ActivityTypeLiveQuery = "live_query" - // ActivityTypeUserAddedBySSO is the activity type for new users added - // via SSO JIT provisioning - ActivityTypeUserAddedBySSO = "user_added_by_sso" - // ActivityTypeEditedAgentOptions is the activity type for when the agent - // options are edited (either globally or for a team). - ActivityTypeEditedAgentOptions = "edited_agent_options" - // ActivityTypeAppliedSpecTeam is the activity type for a team spec applied - ActivityTypeAppliedSpecTeam = "applied_spec_team" - // ActivityTypeCreatedUser is the activity type for created users. - ActivityTypeCreatedUser = "created_user" - // ActivityTypeDeletedUser is the activity type for deleted users. - ActivityTypeDeletedUser = "deleted_user" - // ActivityTypeChangedUserGlobalRole is the activity type for changed user global role. - ActivityTypeChangedUserGlobalRole = "changed_user_global_role" - // ActivityTypeDeletedUserGlobalRole is the activity type for deleted user global role. - ActivityTypeDeletedUserGlobalRole = "deleted_user_global_role" - // ActivityTypeChangedUserTeamRole is the activity type for changed user team role. - ActivityTypeChangedUserTeamRole = "changed_user_team_role" - // ActivityTypeDeletedUserTeamRole is the activity type for deleted user team role. - ActivityTypeDeletedUserTeamRole = "deleted_user_team_role" -) +//go:generate go run gen_activity_doc.go ../../docs/Using-Fleet/Audit-Activities.md + +// ActivityDetailsList is used to generate documentation. +var ActivityDetailsList = []ActivityDetails{ + ActivityTypeCreatedPack{}, + ActivityTypeEditedPack{}, + ActivityTypeDeletedPack{}, + ActivityTypeAppliedSpecPack{}, + + ActivityTypeCreatedPolicy{}, + ActivityTypeEditedPolicy{}, + ActivityTypeDeletedPolicy{}, + ActivityTypeAppliedSpecPolicy{}, + ActivityTypeCreatedSavedQuery{}, + ActivityTypeEditedSavedQuery{}, + ActivityTypeDeletedSavedQuery{}, + ActivityTypeDeletedMultipleSavedQuery{}, + ActivityTypeAppliedSpecSavedQuery{}, + + ActivityTypeCreatedTeam{}, + ActivityTypeDeletedTeam{}, + ActivityTypeAppliedSpecTeam{}, + + ActivityTypeEditedAgentOptions{}, + + ActivityTypeLiveQuery{}, + + ActivityTypeUserAddedBySSO{}, + + ActivityTypeUserLoggedIn{}, + + ActivityTypeCreatedUser{}, + ActivityTypeDeletedUser{}, + ActivityTypeChangedUserGlobalRole{}, + ActivityTypeDeletedUserGlobalRole{}, + ActivityTypeChangedUserTeamRole{}, + ActivityTypeDeletedUserTeamRole{}, +} + +type ActivityDetails interface { + // ActivityName is the name/type of the activity. + ActivityName() string + // Documentation is used by "go generate" to generate markdown docs. + Documentation() (activity string, details string, detailsExample string) +} + +type ActivityTypeCreatedPack struct { + ID uint `json:"pack_id"` + Name string `json:"pack_name"` +} + +func (a ActivityTypeCreatedPack) ActivityName() string { + return "created_pack" +} + +func (a ActivityTypeCreatedPack) Documentation() (activity string, details string, detailsExample string) { + return `Generated when creating scheduled query packs.`, + `This activity contains the following fields: +- "pack_id": the id of the created pack. +- "pack_name": the name of the created pack.`, `{ + "pack_id": 123, + "pack_name": "foo" +}` +} + +type ActivityTypeEditedPack struct { + ID uint `json:"pack_id"` + Name string `json:"pack_name"` +} + +func (a ActivityTypeEditedPack) ActivityName() string { + return "edited_pack" +} + +func (a ActivityTypeEditedPack) Documentation() (activity string, details string, detailsExample string) { + return `Generated when editing scheduled query packs.`, + `This activity contains the following fields: +- "pack_id": the id of the edited pack. +- "pack_name": the name of the edited pack.`, `{ + "pack_id": 123, + "pack_name": "foo" +}` +} + +type ActivityTypeDeletedPack struct { + Name string `json:"pack_name"` +} + +func (a ActivityTypeDeletedPack) ActivityName() string { + return "deleted_pack" +} + +func (a ActivityTypeDeletedPack) Documentation() (activity string, details string, detailsExample string) { + return `Generated when deleting scheduled query packs.`, + `This activity contains the following fields: +- "pack_name": the name of the created pack.`, `{ + "pack_name": "foo" +}` +} + +type ActivityTypeAppliedSpecPack struct{} + +func (a ActivityTypeAppliedSpecPack) ActivityName() string { + return "applied_spec_pack" +} + +func (a ActivityTypeAppliedSpecPack) Documentation() (activity string, details string, detailsExample string) { + return `Generated when applying a scheduled query pack spec.`, + `This activity does not contain any detail fields.`, "" +} + +type ActivityTypeCreatedPolicy struct { + ID uint `json:"policy_id"` + Name string `json:"policy_name"` +} + +func (a ActivityTypeCreatedPolicy) ActivityName() string { + return "created_policy" +} + +func (a ActivityTypeCreatedPolicy) Documentation() (activity string, details string, detailsExample string) { + return `Generated when creating policies.`, + `This activity contains the following fields: +- "policy_id": the ID of the created policy. +- "policy_name": the name of the created policy.`, `{ + "policy_id": 123, + "policy_name": "foo" +}` +} + +type ActivityTypeEditedPolicy struct { + ID uint `json:"policy_id"` + Name string `json:"policy_name"` +} + +func (a ActivityTypeEditedPolicy) ActivityName() string { + return "edited_policy" +} + +func (a ActivityTypeEditedPolicy) Documentation() (activity string, details string, detailsExample string) { + return `Generated when editing policies.`, + `This activity contains the following fields: +- "policy_id": the ID of the edited policy. +- "policy_name": the name of the edited policy.`, `{ + "policy_id": 123, + "policy_name": "foo" +}` +} + +type ActivityTypeDeletedPolicy struct { + ID uint `json:"policy_id"` + Name string `json:"policy_name"` +} + +func (a ActivityTypeDeletedPolicy) ActivityName() string { + return "deleted_policy" +} + +func (a ActivityTypeDeletedPolicy) Documentation() (activity string, details string, detailsExample string) { + return `Generated when deleting policies.`, + `This activity contains the following fields: +- "policy_id": the ID of the deleted policy. +- "policy_name": the name of the deleted policy.`, `{ + "policy_id": 123, + "policy_name": "foo" +}` +} + +type ActivityTypeAppliedSpecPolicy struct { + Policies []*PolicySpec `json:"policies"` +} + +func (a ActivityTypeAppliedSpecPolicy) ActivityName() string { + return "applied_spec_policy" +} + +func (a ActivityTypeAppliedSpecPolicy) Documentation() (activity string, details string, detailsExample string) { + return `Generated when applying policy specs.`, + `This activity contains a field "policies" where each item is a policy spec with the following fields: +- "name": Name of the applied policy. +- "query": SQL query of the policy. +- "description": Description of the policy. +- "critical": Marks the policy as high impact. +- "resolution": Describes how to solve a failing policy. +- "team": Name of the team this policy belongs to. +- "platform": Comma-separated string to indicate the target platforms. +`, `{ + "policies": [ + { + "name":"Gatekeeper enabled (macOS)", + "query":"SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;", + "critical":false, + "platform":"darwin", + "resolution":"To enable Gatekeeper, on the failing device [...]", + "description":"Checks to make sure that the Gatekeeper feature is [...]" + }, + { + "name":"Full disk encryption enabled (Windows)", + "query":"SELECT 1 FROM bitlocker_info WHERE drive_letter='C:' AND protection_status=1;", + "critical":false, + "platform":"windows", + "resolution":"To get additional information, run the following osquery [...]", + "description":"Checks to make sure that full disk encryption is enabled on Windows devices." + } + ] +}` +} + +type ActivityTypeCreatedSavedQuery struct { + ID uint `json:"query_id"` + Name string `json:"query_name"` +} + +func (a ActivityTypeCreatedSavedQuery) ActivityName() string { + return "created_saved_query" +} + +func (a ActivityTypeCreatedSavedQuery) Documentation() (activity string, details string, detailsExample string) { + return `Generated when creating a new query.`, + `This activity contains the following fields: +- "query_id": the ID of the created query. +- "query_name": the name of the created query.`, `{ + "query_id": 123, + "query_name": "foo" +}` +} + +type ActivityTypeEditedSavedQuery struct { + ID uint `json:"query_id"` + Name string `json:"query_name"` +} + +func (a ActivityTypeEditedSavedQuery) ActivityName() string { + return "edited_saved_query" +} + +func (a ActivityTypeEditedSavedQuery) Documentation() (activity string, details string, detailsExample string) { + return `Generated when editing a saved query.`, + `This activity contains the following fields: +- "query_id": the ID of the query being edited. +- "query_name": the name of the query being edited.`, `{ + "query_id": 123, + "query_name": "foo" +}` +} + +type ActivityTypeDeletedSavedQuery struct { + Name string `json:"query_name"` +} + +func (a ActivityTypeDeletedSavedQuery) ActivityName() string { + return "deleted_saved_query" +} + +func (a ActivityTypeDeletedSavedQuery) Documentation() (activity string, details string, detailsExample string) { + return `Generated when deleting a saved query.`, + `This activity contains the following fields: +- "query_name": the name of the query being deleted.`, `{ + "query_name": "foo" +}` +} + +type ActivityTypeDeletedMultipleSavedQuery struct { + IDs []uint `json:"query_ids"` +} + +func (a ActivityTypeDeletedMultipleSavedQuery) ActivityName() string { + return "deleted_multiple_saved_query" +} + +func (a ActivityTypeDeletedMultipleSavedQuery) Documentation() (activity string, details string, detailsExample string) { + return `Generated when deleting multiple saved queries.`, + `This activity contains the following fields: +- "query_ids": list of IDs of the deleted saved queries.`, `{ + "query_ids": [1, 42, 100] +}` +} + +type ActivityTypeAppliedSpecSavedQuery struct { + Specs []*QuerySpec `json:"specs"` +} + +func (a ActivityTypeAppliedSpecSavedQuery) ActivityName() string { + return "applied_spec_saved_query" +} + +func (a ActivityTypeAppliedSpecSavedQuery) Documentation() (activity string, details string, detailsExample string) { + return `Generated when applying a query spec.`, + `This activity contains a field "specs" where each item is a query spec with the following fields: +- "name": Name of the query. +- "description": Description of the query. +- "query": SQL query.`, `{ + "specs": [ + { + "name":"Get OpenSSL versions", + "query":"SELECT name AS name, version AS version, 'deb_packages' AS source FROM [...]", + "description":"Retrieves the OpenSSL version." + } + ] +}` +} + +type ActivityTypeCreatedTeam struct { + ID uint `json:"team_id"` + Name string `json:"team_name"` +} + +func (a ActivityTypeCreatedTeam) ActivityName() string { + return "created_team" +} + +func (a ActivityTypeCreatedTeam) Documentation() (activity string, details string, detailsExample string) { + return `Generated when creating teams.`, + `This activity contains the following fields: +- "team_id": unique ID of the created team. +- "team_name": the name of the created team.`, `{ + "team_id": 123, + "team_name": "foo" +}` +} + +type ActivityTypeDeletedTeam struct { + ID uint `json:"team_id"` + Name string `json:"team_name"` +} + +func (a ActivityTypeDeletedTeam) ActivityName() string { + return "deleted_team" +} + +func (a ActivityTypeDeletedTeam) Documentation() (activity string, details string, detailsExample string) { + return `Generated when deleting teams.`, + `This activity contains the following fields: +- "team_id": unique ID of the deleted team. +- "team_name": the name of the deleted team.`, `{ + "team_id": 123, + "team_name": "foo" +}` +} + +type TeamActivityDetail struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type ActivityTypeAppliedSpecTeam struct { + Teams []TeamActivityDetail `json:"teams"` +} + +func (a ActivityTypeAppliedSpecTeam) ActivityName() string { + return "applied_spec_team" +} + +func (a ActivityTypeAppliedSpecTeam) Documentation() (activity string, details string, detailsExample string) { + return `Generated when applying team specs.`, + `This activity contains a field "teams" where each item contains the team details with the following fields: +- "id": Unique ID of the team. +- "name": Name of the team.`, `{ + "teams": [ + { + "id": 123, + "name": "foo" + } + ] +}` +} + +type ActivityTypeEditedAgentOptions struct { + Global bool `json:"global"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeEditedAgentOptions) ActivityName() string { + return "edited_agent_options" +} + +func (a ActivityTypeEditedAgentOptions) Documentation() (activity string, details string, detailsExample string) { + return `Generated when agent options are edited (either globally or for a team).`, + `This activity contains the following fields: +- "global": "true" if the user updated the global agent options, "false" if the agent options of a team were updated. +- "team_id": unique ID of the team for which the agent options were updated (null if global is true). +- "team_name": the name of the team for which the agent options were updated (null if global is true).`, `{ + "team_id": 123, + "team_name": "foo", + "global": false +}` +} + +type ActivityTypeLiveQuery struct { + TargetsCount uint `json:"targets_count"` + QuerySQL string `json:"query_sql"` + QueryName *string `json:"query_name,omitempty"` +} + +func (a ActivityTypeLiveQuery) ActivityName() string { + return "live_query" +} + +func (a ActivityTypeLiveQuery) Documentation() (activity string, details string, detailsExample string) { + return `Generated when running live queries.`, + `This activity contains the following fields: +- "targets_count": Number of hosts where the live query was targeted to run. +- "query_sql": The SQL query to run on hosts. +- "query_name": Name of the query (this field is not set if this was not a saved query).`, `{ + "targets_count": 5000, + "query_sql": "SELECT * from osquery_info;", + "query_name": "foo" +}` +} + +type ActivityTypeUserAddedBySSO struct{} + +func (a ActivityTypeUserAddedBySSO) ActivityName() string { + return "user_added_by_sso" +} + +func (a ActivityTypeUserAddedBySSO) Documentation() (activity string, details string, detailsExample string) { + return `Generated when new users are added via SSO JIT provisioning`, + `This activity does not contain any detail fields.`, "" +} type Activity struct { CreateTimestamp @@ -72,6 +437,178 @@ type Activity struct { Details *json.RawMessage `json:"details" db:"details"` } +type ActivityTypeUserLoggedIn struct { + PublicIP string `json:"public_ip"` +} + +func (a ActivityTypeUserLoggedIn) ActivityName() string { + return "user_logged_in" +} + +func (a ActivityTypeUserLoggedIn) Documentation() (activity string, details string, detailsExample string) { + return `Generated when users successfully log in to Fleet.`, + `This activity contains the following fields: +- "public_ip": Public IP of the login request.`, `{ + "public_ip": "168.226.215.82" +}` +} + +type ActivityTypeCreatedUser struct { + UserID uint `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` +} + +func (a ActivityTypeCreatedUser) ActivityName() string { + return "created_user" +} + +func (a ActivityTypeCreatedUser) Documentation() (activity string, details string, detailsExample string) { + return `Generated when a user is created.`, + `This activity contains the following fields: +- "user_id": Unique ID of the created user in Fleet. +- "user_name": Name of the created user. +- "user_email": E-mail of the created user.`, `{ + "user_id": 42, + "user_name": "Foo", + "user_email": "foo@example.com" +}` +} + +type ActivityTypeDeletedUser struct { + UserID uint `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` +} + +func (a ActivityTypeDeletedUser) ActivityName() string { + return "deleted_user" +} + +func (a ActivityTypeDeletedUser) Documentation() (activity string, details string, detailsExample string) { + return `Generated when a user is deleted.`, + `This activity contains the following fields: +- "user_id": Unique ID of the deleted user in Fleet. +- "user_name": Name of the deleted user. +- "user_email": E-mail of the deleted user.`, `{ + "user_id": 42, + "user_name": "Foo", + "user_email": "foo@example.com" +}` +} + +type ActivityTypeChangedUserGlobalRole struct { + UserID uint `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + Role string `json:"role"` +} + +func (a ActivityTypeChangedUserGlobalRole) ActivityName() string { + return "changed_user_global_role" +} + +func (a ActivityTypeChangedUserGlobalRole) Documentation() (activity string, details string, detailsExample string) { + return `Generated when user global roles are changed.`, + `This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": New global role of the edited user.`, `{ + "user_id": 42, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Observer" +}` +} + +type ActivityTypeDeletedUserGlobalRole struct { + UserID uint `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + OldRole string `json:"role"` +} + +func (a ActivityTypeDeletedUserGlobalRole) ActivityName() string { + return "deleted_user_global_role" +} + +func (a ActivityTypeDeletedUserGlobalRole) Documentation() (activity string, details string, detailsExample string) { + return `Generated when user global roles are deleted.`, + `This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": Deleted global role of the edited user.`, `{ + "user_id": 43, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Maintainer" +}` +} + +type ActivityTypeChangedUserTeamRole struct { + UserID uint `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + Role string `json:"role"` + TeamID uint `json:"team_id"` + TeamName string `json:"team_name"` +} + +func (a ActivityTypeChangedUserTeamRole) ActivityName() string { + return "changed_user_team_role" +} + +func (a ActivityTypeChangedUserTeamRole) Documentation() (activity string, details string, detailsExample string) { + return `Generated when user team roles are changed.`, + `This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": Team role set to the edited user. +- "team_id": Unique ID of the team of the changed role. +- "team_name": Name of the team of the changed role.`, `{ + "user_id": 43, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Maintainer", + "team_id": 5, + "team_name": "Bar" +}` +} + +type ActivityTypeDeletedUserTeamRole struct { + UserID uint `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + Role string `json:"role"` + TeamID uint `json:"team_id"` + TeamName string `json:"team_name"` +} + +func (a ActivityTypeDeletedUserTeamRole) ActivityName() string { + return "deleted_user_team_role" +} + +func (a ActivityTypeDeletedUserTeamRole) Documentation() (activity string, details string, detailsExample string) { + return `Generated when user team roles are deleted.`, + `This activity contains the following fields: +- "user_id": Unique ID of the edited user in Fleet. +- "user_name": Name of the edited user. +- "user_email": E-mail of the edited user. +- "role": Team role deleted from the edited user. +- "team_id": Unique ID of the team of the deleted role. +- "team_name": Name of the team of the deleted role.`, `{ + "user_id": 44, + "user_name": "Foo", + "user_email": "foo@example.com", + "role": "Observer", + "team_id": 2, + "team_name": "Zoo" +}` +} + // AuthzType implement AuthzTyper to be able to verify access to activities func (*Activity) AuthzType() string { return "activity" diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index ec12736ef3..55f6746635 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -437,7 +437,7 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // ActivitiesStore - NewActivity(ctx context.Context, user *User, activityType string, details *map[string]interface{}) error + NewActivity(ctx context.Context, user *User, activity ActivityDetails) error ListActivities(ctx context.Context, opt ListOptions) ([]*Activity, error) /////////////////////////////////////////////////////////////////////////////// diff --git a/server/fleet/gen_activity_doc.go b/server/fleet/gen_activity_doc.go new file mode 100644 index 0000000000..8abeda8836 --- /dev/null +++ b/server/fleet/gen_activity_doc.go @@ -0,0 +1,73 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +func main() { + var b strings.Builder + + b.WriteString(` +# Audit Activities + +Fleet logs the following information for administrative actions (in JSON): + +- ` + "`" + `created_at` + "`" + `: Timestamp of the event. +- ` + "`" + `id` + "`" + `: Unique ID of the generated event in Fleet. +- ` + "`" + `actor_full_name` + "`" + `: Author user name (missing if the user was deleted). +- ` + "`" + `actor_id` + "`" + `: Unique ID of the author in Fleet (missing if the user was deleted). +- ` + "`" + `actor_gravatar` + "`" + `: Gravatar URL of the author (missing if the user was deleted). +- ` + "`" + `actor_email` + "`" + `: E-mail of the author (missing if the user was deleted). +- ` + "`" + `type` + "`" + `: Type of the activity (see all types below). +- ` + "`" + `details` + "`" + `: Specific details depending on the type of activity (see details for each activity type below). + +Example: +` + "```" + `json +{ + "created_at": "2022-12-20T14:54:17Z", + "id": 6, + "actor_full_name": "Gandalf", + "actor_id": 2, + "actor_gravatar": "foo@example.com", + "actor_email": "foo@example.com", + "type": "edited_saved_query", + "details":{ + "query_id": 42, + "query_name": "Some query name" + } +} +` + "```" + ` + +## List of activities and their specific details + +`) + + activityMap := map[string]struct{}{} + for _, activity := range fleet.ActivityDetailsList { + if _, ok := activityMap[activity.ActivityName()]; ok { + panic(fmt.Sprintf("type %s already used", activity.ActivityName())) + } + activityMap[activity.ActivityName()] = struct{}{} + + fmt.Fprintf(&b, "### Type `%s`\n\n", activity.ActivityName()) + activityTypeDoc, detailsDoc, detailsExampleDoc := activity.Documentation() + fmt.Fprintf(&b, activityTypeDoc+"\n\n"+detailsDoc+"\n\n") + if detailsExampleDoc != "" { + fmt.Fprintf(&b, "#### Example\n\n```json\n%s\n```\n\n", detailsExampleDoc) + } + } + b.WriteString(` + +`) + + if err := os.WriteFile(os.Args[1], []byte(b.String()), 0600); err != nil { + panic(err) + } +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 2c9e16e099..1a1f463480 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -335,7 +335,7 @@ type UpdateHostOperatingSystemFunc func(ctx context.Context, hostID uint, hostOS type CleanupHostOperatingSystemsFunc func(ctx context.Context) error -type NewActivityFunc func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error +type NewActivityFunc func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error type ListActivitiesFunc func(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Activity, error) @@ -2070,9 +2070,9 @@ func (s *DataStore) CleanupHostOperatingSystems(ctx context.Context) error { return s.CleanupHostOperatingSystemsFunc(ctx) } -func (s *DataStore) NewActivity(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { +func (s *DataStore) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { s.NewActivityFuncInvoked = true - return s.NewActivityFunc(ctx, user, activityType, details) + return s.NewActivityFunc(ctx, user, activity) } func (s *DataStore) ListActivities(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Activity, error) { diff --git a/server/service/activities.go b/server/service/activities.go index 7a7825fd9b..6fed0479b8 100644 --- a/server/service/activities.go +++ b/server/service/activities.go @@ -45,8 +45,12 @@ func logRoleChangeActivities(ctx context.Context, ds fleet.Datastore, adminUser if err := ds.NewActivity( ctx, adminUser, - fleet.ActivityTypeChangedUserGlobalRole, - &map[string]interface{}{"user_name": user.Name, "user_id": user.ID, "user_email": user.Email, "role": *user.GlobalRole}, + fleet.ActivityTypeChangedUserGlobalRole{ + UserID: user.ID, + UserName: user.Name, + UserEmail: user.Email, + Role: *user.GlobalRole, + }, ); err != nil { return err } @@ -55,8 +59,12 @@ func logRoleChangeActivities(ctx context.Context, ds fleet.Datastore, adminUser if err := ds.NewActivity( ctx, adminUser, - fleet.ActivityTypeDeletedUserGlobalRole, - &map[string]interface{}{"user_name": user.Name, "user_id": user.ID, "user_email": user.Email, "role": *oldRole}, + fleet.ActivityTypeDeletedUserGlobalRole{ + UserID: user.ID, + UserName: user.Name, + UserEmail: user.Email, + OldRole: *oldRole, + }, ); err != nil { return err } @@ -76,8 +84,14 @@ func logRoleChangeActivities(ctx context.Context, ds fleet.Datastore, adminUser if err := ds.NewActivity( ctx, adminUser, - fleet.ActivityTypeChangedUserTeamRole, - &map[string]interface{}{"user_name": user.Name, "user_id": user.ID, "user_email": user.Email, "team_name": t.Name, "team_id": t.ID, "role": t.Role}, + fleet.ActivityTypeChangedUserTeamRole{ + UserID: user.ID, + UserName: user.Name, + UserEmail: user.Email, + Role: t.Role, + TeamID: t.ID, + TeamName: t.Name, + }, ); err != nil { return err } @@ -89,8 +103,14 @@ func logRoleChangeActivities(ctx context.Context, ds fleet.Datastore, adminUser if err := ds.NewActivity( ctx, adminUser, - fleet.ActivityTypeDeletedUserTeamRole, - &map[string]interface{}{"user_name": user.Name, "user_id": user.ID, "user_email": user.Email, "team_name": o.Name, "team_id": o.ID, "role": o.Role}, + fleet.ActivityTypeDeletedUserTeamRole{ + UserID: user.ID, + UserName: user.Name, + UserEmail: user.Email, + Role: o.Role, + TeamID: o.ID, + TeamName: o.Name, + }, ); err != nil { return err } diff --git a/server/service/activities_test.go b/server/service/activities_test.go index 0eab05caf2..98f8ee8137 100644 --- a/server/service/activities_test.go +++ b/server/service/activities_test.go @@ -107,8 +107,8 @@ func Test_logRoleChangeActivities(t *testing.T) { ctx := context.Background() ds := new(mock.Store) var activities []string - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { - activities = append(activities, activityType) + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + activities = append(activities, activity.ActivityName()) return nil } for _, tt := range tests { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 7484990603..2ef82ca62c 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -412,8 +412,9 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeEditedAgentOptions, - &map[string]interface{}{"global": true, "team_id": nil, "team_name": nil}, + fleet.ActivityTypeEditedAgentOptions{ + Global: true, + }, ); err != nil { return nil, err } diff --git a/server/service/campaigns.go b/server/service/campaigns.go index 7fd4dad804..c8441b18da 100644 --- a/server/service/campaigns.go +++ b/server/service/campaigns.go @@ -162,18 +162,17 @@ func (svc *Service) NewDistributedQueryCampaign(ctx context.Context, queryString return nil, ctxerr.Wrap(ctx, err, "counting hosts") } - activityData := map[string]interface{}{ - "targets_count": campaign.Metrics.TotalHosts, - "query_sql": query.Query, + activityData := fleet.ActivityTypeLiveQuery{ + TargetsCount: campaign.Metrics.TotalHosts, + QuerySQL: query.Query, } if queryID != nil { - activityData["query_name"] = query.Name + activityData.QueryName = &query.Name } if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeLiveQuery, - &activityData, + activityData, ); err != nil { return nil, err } diff --git a/server/service/campaigns_test.go b/server/service/campaigns_test.go index 9218ad101c..1942cbeff6 100644 --- a/server/service/campaigns_test.go +++ b/server/service/campaigns_test.go @@ -2,10 +2,11 @@ package service import ( "context" - "github.com/stretchr/testify/require" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" @@ -85,14 +86,15 @@ func TestLiveQueryAuth(t *testing.T) { return fleet.TargetMetrics{}, nil } var queryName, querySQL string - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { - name := (*details)["query_name"] + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + act := activity.(fleet.ActivityTypeLiveQuery) + name := act.QueryName if name == nil { queryName = "" } else { - queryName = name.(string) + queryName = *name } - querySQL = (*details)["query_sql"].(string) + querySQL = act.QuerySQL return nil } ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) { diff --git a/server/service/global_policies.go b/server/service/global_policies.go index 832b7ccd19..99db247cdf 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -73,8 +73,10 @@ func (svc Service) NewGlobalPolicy(ctx context.Context, p fleet.PolicyPayload) ( if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeCreatedPolicy, - &map[string]interface{}{"policy_id": policy.ID, "policy_name": policy.Name}, + fleet.ActivityTypeCreatedPolicy{ + ID: policy.ID, + Name: policy.Name, + }, ); err != nil { return nil, err } @@ -211,8 +213,10 @@ func (svc Service) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedPolicy, - &map[string]interface{}{"policy_id": id, "policy_name": policiesByID[id].Name}, + fleet.ActivityTypeDeletedPolicy{ + ID: id, + Name: policiesByID[id].Name, + }, ); err != nil { return nil, ctxerr.Wrap(ctx, err, "adding new activity for deleted policy") } @@ -479,7 +483,8 @@ func (svc *Service) ApplyPolicySpecs(ctx context.Context, policies []*fleet.Poli return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeAppliedSpecPolicy, - &map[string]interface{}{"policies": policies}, + fleet.ActivityTypeAppliedSpecPolicy{ + Policies: policies, + }, ) } diff --git a/server/service/global_policies_test.go b/server/service/global_policies_test.go index 698ebcfd05..7ef07dab65 100644 --- a/server/service/global_policies_test.go +++ b/server/service/global_policies_test.go @@ -40,7 +40,7 @@ func TestGlobalPoliciesAuth(t *testing.T) { ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } ds.SavePolicyFunc = func(ctx context.Context, p *fleet.Policy) error { diff --git a/server/service/http_auth_test.go b/server/service/http_auth_test.go index 2c89281f7f..a422a23990 100644 --- a/server/service/http_auth_test.go +++ b/server/service/http_auth_test.go @@ -172,7 +172,7 @@ func setupAuthTest(t *testing.T) (fleet.Datastore, map[string]fleet.User, *httpt sessions[sessionKey] = session return session, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } return ds, usersMap, server diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index cd19bce7f3..a6c0570293 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -2047,18 +2047,17 @@ func (s *integrationTestSuite) TestListActivities() { ctx := context.Background() u := s.users["admin1@example.com"] - details := make(map[string]interface{}) prevActivities, err := s.ds.ListActivities(ctx, fleet.ListOptions{}) require.NoError(t, err) - err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeAppliedSpecPack, &details) + err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeAppliedSpecPack{}) require.NoError(t, err) - err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeDeletedPack, &details) + err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeDeletedPack{}) require.NoError(t, err) - err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeEditedPack, &details) + err = s.ds.NewActivity(ctx, &u, fleet.ActivityTypeEditedPack{}) require.NoError(t, err) lenPage := len(prevActivities) + 2 @@ -2066,16 +2065,16 @@ func (s *integrationTestSuite) TestListActivities() { var listResp listActivitiesResponse s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(lenPage), "order_key", "id") require.Len(t, listResp.Activities, lenPage) - assert.Equal(t, fleet.ActivityTypeAppliedSpecPack, listResp.Activities[lenPage-2].Type) - assert.Equal(t, fleet.ActivityTypeDeletedPack, listResp.Activities[lenPage-1].Type) + assert.Equal(t, fleet.ActivityTypeAppliedSpecPack{}.ActivityName(), listResp.Activities[lenPage-2].Type) + assert.Equal(t, fleet.ActivityTypeDeletedPack{}.ActivityName(), listResp.Activities[lenPage-1].Type) s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(lenPage), "order_key", "id", "page", "1") require.Len(t, listResp.Activities, 1) - assert.Equal(t, fleet.ActivityTypeEditedPack, listResp.Activities[0].Type) + assert.Equal(t, fleet.ActivityTypeEditedPack{}.ActivityName(), listResp.Activities[0].Type) s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listResp, "per_page", "1", "order_key", "id", "order_direction", "desc") require.Len(t, listResp.Activities, 1) - assert.Equal(t, fleet.ActivityTypeEditedPack, listResp.Activities[0].Type) + assert.Equal(t, fleet.ActivityTypeEditedPack{}.ActivityName(), listResp.Activities[0].Type) } func (s *integrationTestSuite) TestListGetCarves() { @@ -4468,7 +4467,7 @@ func (s *integrationTestSuite) TestAppConfig() { s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") if !assert.Len(t, listActivities.Activities, 1) { // if there is an activity, make sure it is not edited_agent_options - require.NotEqual(t, fleet.ActivityTypeEditedAgentOptions, listActivities.Activities[0].Type) + require.NotEqual(t, fleet.ActivityTypeEditedAgentOptions{}.ActivityName(), listActivities.Activities[0].Type) } // and it did not update the appconfig @@ -4488,7 +4487,7 @@ func (s *integrationTestSuite) TestAppConfig() { }`), http.StatusOK, &acResp) s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") require.True(t, len(listActivities.Activities) > 1) - require.Equal(t, fleet.ActivityTypeEditedAgentOptions, listActivities.Activities[0].Type) + require.Equal(t, fleet.ActivityTypeEditedAgentOptions{}.ActivityName(), listActivities.Activities[0].Type) require.NotNil(t, listActivities.Activities[0].Details) assert.JSONEq(t, `{"global": true, "team_id": null, "team_name": null}`, string(*listActivities.Activities[0].Details)) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 6579db12eb..9e99bdd9cc 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -102,7 +102,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { var listActivities listActivitiesResponse s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") require.True(t, len(listActivities.Activities) > 0) - assert.Equal(t, fleet.ActivityTypeAppliedSpecTeam, listActivities.Activities[0].Type) + assert.Equal(t, fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), listActivities.Activities[0].Type) require.NotNil(t, listActivities.Activities[0].Details) assert.JSONEq(t, fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), string(*listActivities.Activities[0].Details)) @@ -217,7 +217,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { // an activity was created for the newly created team via the applied spec s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") require.True(t, len(listActivities.Activities) > 0) - assert.Equal(t, fleet.ActivityTypeAppliedSpecTeam, listActivities.Activities[0].Type) + assert.Equal(t, fleet.ActivityTypeAppliedSpecTeam{}.ActivityName(), listActivities.Activities[0].Type) require.NotNil(t, listActivities.Activities[0].Details) assert.JSONEq(t, fmt.Sprintf(`{"teams": [{"id": %d, "name": %q}]}`, team.ID, team.Name), string(*listActivities.Activities[0].Details)) @@ -667,7 +667,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { var listActivities listActivitiesResponse s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc") require.True(t, len(listActivities.Activities) > 0) - assert.Equal(t, fleet.ActivityTypeEditedAgentOptions, listActivities.Activities[0].Type) + assert.Equal(t, fleet.ActivityTypeEditedAgentOptions{}.ActivityName(), listActivities.Activities[0].Type) require.NotNil(t, listActivities.Activities[0].Details) assert.JSONEq(t, fmt.Sprintf(`{"global": false, "team_id": %d, "team_name": %q}`, tm1ID, team.Name), string(*listActivities.Activities[0].Details)) @@ -1475,7 +1475,7 @@ func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { require.NotEmpty(t, activitiesResp.Activities) require.Condition(t, func() bool { for _, a := range activitiesResp.Activities { - if a.Type == fleet.ActivityTypeUserAddedBySSO && *a.ActorEmail == auth.UserID() { + if (a.Type == fleet.ActivityTypeUserAddedBySSO{}.ActivityName()) && *a.ActorEmail == auth.UserID() { return true } } diff --git a/server/service/integration_sso_test.go b/server/service/integration_sso_test.go index 9ffa7c952a..461d83daf8 100644 --- a/server/service/integration_sso_test.go +++ b/server/service/integration_sso_test.go @@ -139,7 +139,7 @@ func (s *integrationSSOTestSuite) TestSSOLogin() { require.NotEmpty(t, activitiesResp.Activities) require.Condition(t, func() bool { for _, a := range activitiesResp.Activities { - if a.Type == fleet.ActivityTypeUserLoggedIn && *a.ActorEmail == auth.UserID() { + if (a.Type == fleet.ActivityTypeUserLoggedIn{}.ActivityName()) && *a.ActorEmail == auth.UserID() { return true } } diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 9728a9dc9f..3b44bc7618 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -1429,7 +1429,7 @@ func TestNewDistributedQueryCampaign(t *testing.T) { }, }) q := "select year, month, day, hour, minutes, seconds from time" - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } campaign, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}}) @@ -2196,7 +2196,7 @@ func TestObserversCanOnlyRunDistributedCampaigns(t *testing.T) { }) q := "select year, month, day, hour, minutes, seconds from time" - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } _, err := svc.NewDistributedQueryCampaign(viewerCtx, q, nil, fleet.HostTargets{HostIDs: []uint{2}, LabelIDs: []uint{1}}) @@ -2230,7 +2230,7 @@ func TestObserversCanOnlyRunDistributedCampaigns(t *testing.T) { ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) { return []uint{1, 3, 5}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } lq.On("RunQuery", "21", "select 1;", []uint{1, 3, 5}).Return(nil) @@ -2270,7 +2270,7 @@ func TestTeamMaintainerCanRunNewDistributedCampaigns(t *testing.T) { }) q := "select year, month, day, hour, minutes, seconds from time" - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } // var gotQuery *fleet.Query @@ -2288,7 +2288,7 @@ func TestTeamMaintainerCanRunNewDistributedCampaigns(t *testing.T) { ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) { return []uint{1, 3, 5}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } lq.On("RunQuery", "0", "select year, month, day, hour, minutes, seconds from time", []uint{1, 3, 5}).Return(nil) diff --git a/server/service/packs.go b/server/service/packs.go index fcf0a76e22..a61840eca8 100644 --- a/server/service/packs.go +++ b/server/service/packs.go @@ -170,8 +170,10 @@ func (svc *Service) NewPack(ctx context.Context, p fleet.PackPayload) (*fleet.Pa if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeCreatedPack, - &map[string]interface{}{"pack_id": pack.ID, "pack_name": pack.Name}, + fleet.ActivityTypeCreatedPack{ + ID: pack.ID, + Name: pack.Name, + }, ); err != nil { return nil, err } @@ -264,8 +266,10 @@ func (svc *Service) ModifyPack(ctx context.Context, id uint, p fleet.PackPayload if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeEditedPack, - &map[string]interface{}{"pack_id": pack.ID, "pack_name": pack.Name}, + fleet.ActivityTypeEditedPack{ + ID: pack.ID, + Name: pack.Name, + }, ); err != nil { return nil, err } @@ -358,8 +362,9 @@ func (svc *Service) DeletePack(ctx context.Context, name string) error { return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedPack, - &map[string]interface{}{"pack_name": name}, + fleet.ActivityTypeDeletedPack{ + Name: pack.Name, + }, ) } @@ -405,8 +410,9 @@ func (svc *Service) DeletePackByID(ctx context.Context, id uint) error { return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedPack, - &map[string]interface{}{"pack_name": pack.Name}, + fleet.ActivityTypeDeletedPack{ + Name: pack.Name, + }, ) } @@ -480,8 +486,7 @@ func (svc *Service) ApplyPackSpecs(ctx context.Context, specs []*fleet.PackSpec) return result, svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeAppliedSpecPack, - &map[string]interface{}{}, + fleet.ActivityTypeAppliedSpecPack{}, ) } diff --git a/server/service/packs_test.go b/server/service/packs_test.go index 6d4a279a59..e66c0bc5d6 100644 --- a/server/service/packs_test.go +++ b/server/service/packs_test.go @@ -41,7 +41,7 @@ func TestNewPackSavesTargets(t *testing.T) { ds.NewPackFunc = func(ctx context.Context, pack *fleet.Pack, opts ...fleet.OptionalArg) (*fleet.Pack, error) { return pack, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } diff --git a/server/service/queries.go b/server/service/queries.go index d785d63705..4506497647 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -185,8 +185,10 @@ func (svc *Service) NewQuery(ctx context.Context, p fleet.QueryPayload) (*fleet. if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeCreatedSavedQuery, - &map[string]interface{}{"query_id": query.ID, "query_name": query.Name}, + fleet.ActivityTypeCreatedSavedQuery{ + ID: query.ID, + Name: query.Name, + }, ); err != nil { return nil, err } @@ -266,8 +268,10 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeEditedSavedQuery, - &map[string]interface{}{"query_id": query.ID, "query_name": query.Name}, + fleet.ActivityTypeEditedSavedQuery{ + ID: query.ID, + Name: query.Name, + }, ); err != nil { return nil, err } @@ -321,8 +325,9 @@ func (svc *Service) DeleteQuery(ctx context.Context, name string) error { return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedSavedQuery, - &map[string]interface{}{"query_name": name}, + fleet.ActivityTypeDeletedSavedQuery{ + Name: name, + }, ) } @@ -372,8 +377,9 @@ func (svc *Service) DeleteQueryByID(ctx context.Context, id uint) error { return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedSavedQuery, - &map[string]interface{}{"query_name": query.Name}, + fleet.ActivityTypeDeletedSavedQuery{ + Name: query.Name, + }, ) } @@ -427,8 +433,9 @@ func (svc *Service) DeleteQueries(ctx context.Context, ids []uint) (uint, error) err = svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedMultipleSavedQuery, - &map[string]interface{}{"query_ids": ids}, + fleet.ActivityTypeDeletedMultipleSavedQuery{ + IDs: ids, + }, ) if err != nil { return n, err @@ -502,8 +509,9 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe return svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeAppliedSpecSavedQuery, - &map[string]interface{}{"specs": specs}, + fleet.ActivityTypeAppliedSpecSavedQuery{ + Specs: specs, + }, ) } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 23ccae10c9..fe6f601bfd 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -104,7 +104,7 @@ func TestQueryAuth(t *testing.T) { } return &fleet.Query{ID: 8888, AuthorID: ptr.Uint(6666)}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) { diff --git a/server/service/service_campaign_test.go b/server/service/service_campaign_test.go index 01c7dd1fc6..6ceadeeb06 100644 --- a/server/service/service_campaign_test.go +++ b/server/service/service_campaign_test.go @@ -56,7 +56,7 @@ func TestStreamCampaignResultsClosesReditOnWSClose(t *testing.T) { ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) { return fleet.TargetMetrics{TotalHosts: 1}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) { diff --git a/server/service/service_users.go b/server/service/service_users.go index c53d8cfa03..1e947225c0 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -48,8 +48,11 @@ func (svc *Service) NewUser(ctx context.Context, p fleet.UserPayload) (*fleet.Us if err := svc.ds.NewActivity( ctx, adminUser, - fleet.ActivityTypeCreatedUser, - &map[string]interface{}{"user_name": user.Name, "user_id": user.ID, "user_email": user.Email}, + fleet.ActivityTypeCreatedUser{ + UserID: user.ID, + UserName: user.Name, + UserEmail: user.Email, + }, ); err != nil { return nil, err } diff --git a/server/service/sessions.go b/server/service/sessions.go index ceb7d3597d..3672dc3291 100644 --- a/server/service/sessions.go +++ b/server/service/sessions.go @@ -192,8 +192,8 @@ func (svc *Service) Login(ctx context.Context, email, password string) (*fleet.U return nil, nil, fleet.NewAuthFailedError(err.Error()) } - if err := svc.ds.NewActivity(ctx, user, fleet.ActivityTypeUserLoggedIn, &map[string]interface{}{ - "public_ip": publicip.FromContext(ctx), + if err := svc.ds.NewActivity(ctx, user, fleet.ActivityTypeUserLoggedIn{ + PublicIP: publicip.FromContext(ctx), }); err != nil { return nil, nil, err } @@ -515,9 +515,8 @@ func (svc *Service) LoginSSOUser(ctx context.Context, user *fleet.User, redirect err = svc.ds.NewActivity( ctx, user, - fleet.ActivityTypeUserLoggedIn, - &map[string]interface{}{ - "public_ip": publicip.FromContext(ctx), + fleet.ActivityTypeUserLoggedIn{ + PublicIP: publicip.FromContext(ctx), }, ) if err != nil { diff --git a/server/service/team_policies.go b/server/service/team_policies.go index 35ea6728c9..fd8be45c01 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -81,8 +81,10 @@ func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.Polic if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeCreatedPolicy, - &map[string]interface{}{"policy_id": policy.ID, "policy_name": policy.Name}, + fleet.ActivityTypeCreatedPolicy{ + ID: policy.ID, + Name: policy.Name, + }, ); err != nil { return nil, err } @@ -244,8 +246,10 @@ func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []ui if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeDeletedPolicy, - &map[string]interface{}{"policy_id": id, "policy_name": policiesByID[id].Name}, + fleet.ActivityTypeDeletedPolicy{ + ID: id, + Name: policiesByID[id].Name, + }, ); err != nil { return nil, ctxerr.Wrap(ctx, err, "adding new activity for deleted policy") } @@ -337,8 +341,10 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), - fleet.ActivityTypeEditedPolicy, - &map[string]interface{}{"policy_id": policy.ID, "policy_name": policy.Name}, + fleet.ActivityTypeEditedPolicy{ + ID: policy.ID, + Name: policy.Name, + }, ); err != nil { return nil, err } diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go index 1c57ba3c2e..e9e218e90d 100644 --- a/server/service/team_policies_test.go +++ b/server/service/team_policies_test.go @@ -56,7 +56,7 @@ func TestTeamPoliciesAuth(t *testing.T) { ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error { return nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { diff --git a/server/service/teams_test.go b/server/service/teams_test.go index 06e1cad2b0..88a898d4db 100644 --- a/server/service/teams_test.go +++ b/server/service/teams_test.go @@ -25,7 +25,7 @@ func TestTeamAuth(t *testing.T) { ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { return &fleet.Team{}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { @@ -264,8 +264,9 @@ func TestApplyTeamSpecs(t *testing.T) { return team, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { - require.Len(t, (*details)["teams"], 1) + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + act := activity.(fleet.ActivityTypeAppliedSpecTeam) + require.Len(t, act.Teams, 1) return nil } @@ -344,8 +345,9 @@ func TestApplyTeamSpecs(t *testing.T) { return &fleet.Team{}, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { - require.Len(t, (*details)["teams"], 1) + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + act := activity.(fleet.ActivityTypeAppliedSpecTeam) + require.Len(t, act.Teams, 1) return nil } diff --git a/server/service/users.go b/server/service/users.go index c71554b091..0a427c3fb8 100644 --- a/server/service/users.go +++ b/server/service/users.go @@ -431,8 +431,11 @@ func (svc *Service) DeleteUser(ctx context.Context, id uint) error { if err := svc.ds.NewActivity( ctx, adminUser, - fleet.ActivityTypeDeletedUser, - &map[string]interface{}{"user_name": user.Name, "user_id": user.ID, "user_email": user.Email}, + fleet.ActivityTypeDeletedUser{ + UserID: user.ID, + UserName: user.Name, + UserEmail: user.Email, + }, ); err != nil { return err } diff --git a/server/service/users_test.go b/server/service/users_test.go index f16c678e54..460471569d 100644 --- a/server/service/users_test.go +++ b/server/service/users_test.go @@ -77,7 +77,7 @@ func TestUserAuth(t *testing.T) { ds.ListSessionsForUserFunc = func(ctx context.Context, id uint) ([]*fleet.Session, error) { return nil, nil } - ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error { + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil }