diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md index 59e2fd5c80..ab4c09fdf7 100644 --- a/docs/Using-Fleet/Audit-Activities.md +++ b/docs/Using-Fleet/Audit-Activities.md @@ -731,6 +731,44 @@ This activity contains the following fields: } ``` +### Type `added_bootstrap_package` + +Generated when a user adds a new bootstrap package to a team (or no team). + +This activity contains the following fields: +- "package_name": Name of the package. +- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team. + +#### Example + +```json +{ + "bootstrap_package_name": "bootstrap-package.pkg", + "team_id": 123, + "team_name": "Workstations" +} +``` + +### Type `deleted_bootstrap_package` + +Generated when a user deletes a bootstrap package from a team (or no team). + +This activity contains the following fields: +- "package_name": Name of the package. +- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team. + +#### Example + +```json +{ + "package_name": "bootstrap-package.pkg", + "team_id": 123, + "team_name": "Workstations" +} +``` + \ No newline at end of file diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 7498ae15a0..59a16a5aaf 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -155,6 +155,17 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str return err } + var ptrTeamName *string + var ptrTeamId *uint + if teamID >= 1 { + tm, err := svc.teamByIDOrName(ctx, &teamID, nil) + if err != nil { + return ctxerr.Wrap(ctx, err, "get team name for upload bootstrap package activity details") + } + ptrTeamName = &tm.Name + ptrTeamId = &teamID + } + hashBuf := bytes.NewBuffer(nil) if err := file.CheckPKGSignature(io.TeeReader(pkg, hashBuf)); err != nil { msg := "invalid package" @@ -185,6 +196,10 @@ func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name str return err } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeAddedBootstrapPackage{BootstrapPackageName: name, TeamID: ptrTeamId, TeamName: ptrTeamName}); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for upload bootstrap package") + } + return nil } @@ -217,10 +232,30 @@ func (svc *Service) DeleteMDMAppleBootstrapPackage(ctx context.Context, teamID u return err } + var ptrTeamName *string + var ptrTeamId *uint + if teamID >= 1 { + tm, err := svc.teamByIDOrName(ctx, &teamID, nil) + if err != nil { + return ctxerr.Wrap(ctx, err, "get team name for delete bootstrap package activity details") + } + ptrTeamName = &tm.Name + ptrTeamId = &teamID + } + + meta, err := svc.ds.GetMDMAppleBootstrapPackageMeta(ctx, teamID) + if err != nil { + return ctxerr.Wrap(ctx, err, "fetching bootstrap package metadata") + } + if err := svc.ds.DeleteMDMAppleBootstrapPackage(ctx, teamID); err != nil { return ctxerr.Wrap(ctx, err, "deleting bootstrap package") } + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDeletedBootstrapPackage{BootstrapPackageName: meta.Name, TeamID: ptrTeamId, TeamName: ptrTeamName}); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for delete bootstrap package") + } + return nil } diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 1a8dc517e4..b8db408fdc 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -39,6 +39,8 @@ export enum ActivityType { EditedMacOSProfile = "edited_macos_profile", EnabledMacDiskEncryption = "enabled_macos_disk_encryption", DisabledMacDiskEncryption = "disabled_macos_disk_encryption", + AddedBootstrapPackage = "added_bootstrap_package", + DeletedBootstrapPackage = "deleted_bootstrap_package", ChangedMacOSSetupAssistant = "changed_macos_setup_assistant", DeletedMacOSSetupAssistant = "deleted_macos_setup_assistant", } @@ -77,5 +79,6 @@ export interface IActivityDetails { deadline?: string; profile_name?: string; profile_identifier?: string; + package_name?: string; name?: string; } diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx index 0fc6a62ca5..041270289b 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx @@ -488,4 +488,86 @@ describe("Activity Feed", () => { }) ).toBeInTheDocument(); }); + + it("renders a 'added_bootstrap_package' type activity for a team", () => { + const activity = createMockActivity({ + type: ActivityType.AddedBootstrapPackage, + details: { package_name: "foo.pkg", team_name: "Alphas" }, + }); + render(); + + expect( + screen.getByText("added a bootstrap package (", { exact: false }) + ).toBeInTheDocument(); + expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument(); + expect( + screen.getByText(") for macOS hosts that automatically enroll to the ", { + exact: false, + }) + ).toBeInTheDocument(); + expect(screen.getByText("Alphas")).toBeInTheDocument(); + expect(screen.getByText(" team.", { exact: false })).toBeInTheDocument(); + const withNoTeams = screen.queryByText("automatically enroll to no team"); + expect(withNoTeams).toBeNull(); + }); + + it("renders a 'deleted_bootstrap_package' type activity for a team", () => { + const activity = createMockActivity({ + type: ActivityType.DeletedBootstrapPackage, + details: { package_name: "foo.pkg", team_name: "Alphas" }, + }); + render(); + + expect( + screen.getByText("deleted a bootstrap package (", { exact: false }) + ).toBeInTheDocument(); + expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument(); + expect( + screen.getByText(") for macOS hosts that automatically enroll to the ", { + exact: false, + }) + ).toBeInTheDocument(); + expect(screen.getByText("Alphas")).toBeInTheDocument(); + expect(screen.getByText(" team.", { exact: false })).toBeInTheDocument(); + const withNoTeams = screen.queryByText("automatically enroll to no team"); + expect(withNoTeams).toBeNull(); + }); + + it("renders a 'added_bootstrap_package' type activity for hosts with no team.", () => { + const activity = createMockActivity({ + type: ActivityType.AddedBootstrapPackage, + details: { package_name: "foo.pkg" }, + }); + render(); + + expect( + screen.getByText("added a bootstrap package (", { exact: false }) + ).toBeInTheDocument(); + expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument(); + expect( + screen.getByText( + ") for macOS hosts that automatically enroll to no team.", + { exact: false } + ) + ).toBeInTheDocument(); + }); + + it("renders a 'deleted_bootstrap_package' type activity for hosts with no team.", () => { + const activity = createMockActivity({ + type: ActivityType.DeletedBootstrapPackage, + details: { package_name: "foo.pkg" }, + }); + render(); + + expect( + screen.getByText("deleted a bootstrap package (", { exact: false }) + ).toBeInTheDocument(); + expect(screen.getByText("foo.pkg", { exact: false })).toBeInTheDocument(); + expect( + screen.getByText( + ") for macOS hosts that automatically enroll to no team.", + { exact: false } + ) + ).toBeInTheDocument(); + }); }); diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index c0e1cadd54..305c975d41 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -345,6 +345,56 @@ const TAGGED_TEMPLATES = { ); }, + addedMDMBootstrapPackage: (activity: IActivity) => { + const packageName = activity.details?.package_name; + return ( + <> + {" "} + added a bootstrap package{" "} + {packageName ? ( + <> + ({packageName}){" "} + + ) : ( + "" + )} + for macOS hosts that automatically enroll to{" "} + {activity.details?.team_name ? ( + <> + the {activity.details.team_name} team + + ) : ( + "no team" + )} + . + + ); + }, + deletedMDMBootstrapPackage: (activity: IActivity) => { + const packageName = activity.details?.package_name; + return ( + <> + {" "} + deleted a bootstrap package{" "} + {packageName ? ( + <> + ({packageName}){" "} + + ) : ( + "" + )} + for macOS hosts that automatically enroll to{" "} + {activity.details?.team_name ? ( + <> + the {activity.details.team_name} team + + ) : ( + "no team" + )} + . + + ); + }, }; const getDetail = ( @@ -428,6 +478,12 @@ const getDetail = ( case ActivityType.DisabledMacDiskEncryption: { return TAGGED_TEMPLATES.disableMacDiskEncryption(activity); } + case ActivityType.AddedBootstrapPackage: { + return TAGGED_TEMPLATES.addedMDMBootstrapPackage(activity); + } + case ActivityType.DeletedBootstrapPackage: { + return TAGGED_TEMPLATES.deletedMDMBootstrapPackage(activity); + } case ActivityType.ChangedMacOSSetupAssistant: { return TAGGED_TEMPLATES.changedMacOSSetupAssistant(activity); } diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 539a777eef..d7bb0174f4 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -60,6 +60,9 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeEnabledMacosDiskEncryption{}, ActivityTypeDisabledMacosDiskEncryption{}, + + ActivityTypeAddedBootstrapPackage{}, + ActivityTypeDeletedBootstrapPackage{}, } type ActivityDetails interface { @@ -892,6 +895,50 @@ func (a ActivityTypeDisabledMacosDiskEncryption) Documentation() (activity, deta }` } +type ActivityTypeAddedBootstrapPackage struct { + BootstrapPackageName string `json:"bootstrap_package_name"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeAddedBootstrapPackage) ActivityName() string { + return "added_bootstrap_package" +} + +func (a ActivityTypeAddedBootstrapPackage) Documentation() (activity, details, detailsExample string) { + return `Generated when a user adds a new bootstrap package to a team (or no team).`, + `This activity contains the following fields: +- "package_name": Name of the package. +- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team.`, `{ + "bootstrap_package_name": "bootstrap-package.pkg", + "team_id": 123, + "team_name": "Workstations" +}` +} + +type ActivityTypeDeletedBootstrapPackage struct { + BootstrapPackageName string `json:"bootstrap_package_name"` + TeamID *uint `json:"team_id"` + TeamName *string `json:"team_name"` +} + +func (a ActivityTypeDeletedBootstrapPackage) ActivityName() string { + return "deleted_bootstrap_package" +} + +func (a ActivityTypeDeletedBootstrapPackage) Documentation() (activity, details, detailsExample string) { + return `Generated when a user deletes a bootstrap package from a team (or no team).`, + `This activity contains the following fields: +- "package_name": Name of the package. +- "team_id": The ID of the team that the package applies to, null if it applies to devices that are not in a team. +- "team_name": The name of the team that the package applies to, null if it applies to devices that are not in a team.`, `{ + "package_name": "bootstrap-package.pkg", + "team_id": 123, + "team_name": "Workstations" +}` +} + // LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams. func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error { if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 93b24f72db..af9efb8f1e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -2619,6 +2619,12 @@ func (s *integrationMDMTestSuite) TestBootstrapPackage() { s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: wrongTOCPkg, Name: "pkg.pkg"}, http.StatusBadRequest, "invalid package") // successfully upload a package s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg, Name: "pkg.pkg", TeamID: 0}, http.StatusOK, "") + // check the activity log + s.lastActivityMatches( + fleet.ActivityTypeAddedBootstrapPackage{}.ActivityName(), + `{"bootstrap_package_name": "pkg.pkg", "team_id": null, "team_name": null}`, + 0, + ) // get package metadata var metadataResp bootstrapPackageMetadataResponse @@ -2643,6 +2649,13 @@ func (s *integrationMDMTestSuite) TestBootstrapPackage() { // delete package var deleteResp deleteBootstrapPackageResponse s.DoJSON("DELETE", "/api/latest/fleet/mdm/apple/bootstrap/0", nil, http.StatusOK, &deleteResp) + // check the activity log + s.lastActivityMatches( + fleet.ActivityTypeDeletedBootstrapPackage{}.ActivityName(), + `{"bootstrap_package_name": "pkg.pkg", "team_id": null, "team_name": null}`, + 0, + ) + metadataResp = bootstrapPackageMetadataResponse{} s.DoJSON("GET", "/api/latest/fleet/mdm/apple/bootstrap/0/metadata", nil, http.StatusNotFound, &metadataResp) // trying to delete again is a bad request