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