45640 Add activity feed entry when a custom Apple or Windows MDM command is run (#47743)

**Related issue:** Resolves #45640

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.


## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Running custom MDM commands on Apple or Windows devices now creates
activity log entries that appear in both the global activity feed and
host-specific activity feeds.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Andrew Mellor
2026-06-19 13:42:17 +01:00
committed by GitHub
parent f91df7516d
commit 013ccf406e
5 changed files with 232 additions and 2 deletions
@@ -0,0 +1 @@
* Add activity feed entry when a user runs a custom Apple or Windows MDM command, visible in both the global activity feed and the host's activity feed.
+21
View File
@@ -836,6 +836,27 @@ func (a ActivityTypeRanScript) WasFromAutomation() bool {
return a.PolicyID != nil || a.FromSetupExperience
}
type ActivityTypeRanCustomMDMCommand struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
HostUUID string `json:"host_uuid"`
CommandUUID string `json:"command_uuid"`
RequestType string `json:"request_type"`
Platform string `json:"platform"`
}
func (a ActivityTypeRanCustomMDMCommand) ActivityName() string {
return "ran_custom_mdm_command"
}
func (a ActivityTypeRanCustomMDMCommand) HostIDs() []uint {
return []uint{a.HostID}
}
func (a ActivityTypeRanCustomMDMCommand) HostOnly() bool {
return false
}
type ActivityTypeAddedScript struct {
ScriptName string `json:"script_name"`
TeamID *uint `json:"team_id" renameto:"fleet_id"`
+16
View File
@@ -9796,6 +9796,14 @@ func (s *integrationMDMTestSuite) TestRunMDMCommands() {
require.NotEmpty(t, runResp.CommandUUID)
require.Equal(t, "windows", runResp.Platform)
require.Equal(t, "./SetValues", runResp.RequestType)
s.lastActivityMatches(fleet.ActivityTypeRanCustomMDMCommand{}.ActivityName(), fmt.Sprintf(`{
"host_id": %d,
"host_display_name": %q,
"host_uuid": %q,
"command_uuid": %q,
"request_type": "./SetValues",
"platform": "windows"
}`, enrolledWindows.ID, enrolledWindows.DisplayName(), enrolledWindows.UUID, runResp.CommandUUID), 0)
// valid macOS
runResp = runMDMCommandResponse{}
@@ -9806,6 +9814,14 @@ func (s *integrationMDMTestSuite) TestRunMDMCommands() {
require.NotEmpty(t, runResp.CommandUUID)
require.Equal(t, "darwin", runResp.Platform)
require.Equal(t, "ShutDownDevice", runResp.RequestType)
s.lastActivityMatches(fleet.ActivityTypeRanCustomMDMCommand{}.ActivityName(), fmt.Sprintf(`{
"host_id": %d,
"host_display_name": %q,
"host_uuid": %q,
"command_uuid": %q,
"request_type": "ShutDownDevice",
"platform": "darwin"
}`, enrolledMac.ID, enrolledMac.DisplayName(), enrolledMac.UUID, runResp.CommandUUID), 0)
}
func (s *integrationMDMTestSuite) TestUpdateMDMWindowsEnrollmentsHostUUID() {
+38 -2
View File
@@ -579,13 +579,49 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host
}
}
// Use UUIDs from the resolved hosts so the enqueue and activity creation
// operate on the same validated set, not the raw (potentially duplicate or
// unknown) request input.
resolvedUUIDs := make([]string, len(hosts))
for i, h := range hosts {
resolvedUUIDs[i] = h.UUID
}
// the rest is platform-specific (validation of command payload, enqueueing, etc.)
switch commandPlatform {
case "windows":
return svc.enqueueMicrosoftMDMCommand(ctx, rawXMLCmd, hostUUIDs)
result, err = svc.enqueueMicrosoftMDMCommand(ctx, rawXMLCmd, resolvedUUIDs)
default:
return svc.enqueueAppleMDMCommand(ctx, rawXMLCmd, hostUUIDs)
result, err = svc.enqueueAppleMDMCommand(ctx, rawXMLCmd, resolvedUUIDs)
}
if err != nil {
return nil, err
}
failedUUIDs := make(map[string]struct{}, len(result.FailedUUIDs))
for _, uuid := range result.FailedUUIDs {
failedUUIDs[uuid] = struct{}{}
}
for _, h := range hosts {
if _, failed := failedUUIDs[h.UUID]; failed {
continue
}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeRanCustomMDMCommand{
HostID: h.ID,
HostDisplayName: h.DisplayName(),
HostUUID: h.UUID,
CommandUUID: result.CommandUUID,
RequestType: result.RequestType,
Platform: commandPlatform,
}); err != nil {
// Activity logging is best-effort: the command was already enqueued
// successfully, so returning an error here could cause clients to retry
// and send duplicate MDM commands to devices.
svc.logger.ErrorContext(ctx, "failed to log activity for ran custom mdm command", "err", err, "host_uuid", h.UUID)
}
}
return result, nil
}
// validateAppleMDMCommand validates an Apple MDM command before it is enqueued.
+156
View File
@@ -29,7 +29,10 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
nanomdm_mdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
@@ -649,6 +652,159 @@ func TestRunMDMCommandValidations(t *testing.T) {
}
}
func TestRunMDMCommandCreatesActivity(t *testing.T) {
ds := new(mock.Store)
opts := &TestServerOpts{SkipCreateTestUsers: true}
svc, ctx := newTestService(t, ds, nil, nil, opts)
ctx = test.UserContext(ctx, test.UserAdmin)
windowsHost := &fleet.Host{
ID: 42,
UUID: "win-uuid-1",
Platform: "windows",
Hostname: "DESKTOP-TEST",
ComputerName: "DESKTOP-TEST",
}
ds.ListHostsLiteByUUIDsFunc = func(_ context.Context, _ fleet.TeamFilter, _ []string) ([]*fleet.Host, error) {
return []*fleet.Host{windowsHost}, nil
}
ds.AreHostsConnectedToFleetMDMFunc = func(_ context.Context, _ []*fleet.Host) (map[string]bool, error) {
return map[string]bool{windowsHost.UUID: true}, nil
}
ds.AppConfigFunc = func(_ context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{WindowsEnabledAndConfigured: true},
}, nil
}
ds.MDMWindowsInsertCommandForHostsFunc = func(_ context.Context, _ []string, _ *fleet.MDMWindowsCommand) error {
return nil
}
var capturedUser *activity_api.User
var capturedActivity activity_api.ActivityDetails
opts.ActivityMock.NewActivityFunc = func(_ context.Context, u *activity_api.User, act activity_api.ActivityDetails) error {
capturedUser = u
capturedActivity = act
return nil
}
rawCmd := `<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>./FooBar</LocURI>
</Target>
</Item>
</Exec>`
encoded := base64.StdEncoding.EncodeToString([]byte(rawCmd))
_, err := svc.RunMDMCommand(ctx, encoded, []string{windowsHost.UUID})
require.NoError(t, err)
require.True(t, opts.ActivityMock.NewActivityFuncInvoked)
require.NotNil(t, capturedActivity)
act, ok := capturedActivity.(*fleet.ActivityTypeRanCustomMDMCommand)
require.True(t, ok, "expected *fleet.ActivityTypeRanCustomMDMCommand, got %T", capturedActivity)
assert.Equal(t, windowsHost.ID, act.HostID)
assert.Equal(t, windowsHost.DisplayName(), act.HostDisplayName)
assert.Equal(t, windowsHost.UUID, act.HostUUID)
assert.Equal(t, "./FooBar", act.RequestType)
assert.Equal(t, "windows", act.Platform)
assert.NotEmpty(t, act.CommandUUID)
require.NotNil(t, capturedUser)
assert.Equal(t, test.UserAdmin.ID, capturedUser.ID)
assert.Equal(t, test.UserAdmin.Email, capturedUser.Email)
}
// mockAPNSPusher implements nanomdm_push.Pusher for unit tests, returning a
// push failure for any UUID in failUUIDs and success for all others.
type mockAPNSPusher struct {
failUUIDs map[string]bool
}
func (m *mockAPNSPusher) Push(_ context.Context, ids []string) (map[string]*nanomdm_push.Response, error) {
result := make(map[string]*nanomdm_push.Response, len(ids))
for _, id := range ids {
if m.failUUIDs[id] {
result[id] = &nanomdm_push.Response{Err: errors.New("push failed")}
} else {
result[id] = &nanomdm_push.Response{}
}
}
return result, nil
}
func TestRunMDMCommandSkipsActivityForFailedHosts(t *testing.T) {
ds := new(mock.Store)
mdmStorage := &mdmmock.MDMAppleStore{}
mdmStorage.EnqueueCommandFunc = func(_ context.Context, _ []string, _ *nanomdm_mdm.CommandWithSubtype) (map[string]error, error) {
return nil, nil
}
host1 := &fleet.Host{ID: 1, UUID: "apple-uuid-1", Platform: "darwin", Hostname: "mac1", ComputerName: "mac1"}
host2 := &fleet.Host{ID: 2, UUID: "apple-uuid-2", Platform: "darwin", Hostname: "mac2", ComputerName: "mac2"}
opts := &TestServerOpts{
SkipCreateTestUsers: true,
MDMStorage: mdmStorage,
MDMPusher: &mockAPNSPusher{failUUIDs: map[string]bool{host2.UUID: true}},
}
svc, ctx := newTestService(t, ds, nil, nil, opts)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.ListHostsLiteByUUIDsFunc = func(_ context.Context, _ fleet.TeamFilter, _ []string) ([]*fleet.Host, error) {
return []*fleet.Host{host1, host2}, nil
}
ds.AreHostsConnectedToFleetMDMFunc = func(_ context.Context, _ []*fleet.Host) (map[string]bool, error) {
return map[string]bool{host1.UUID: true, host2.UUID: true}, nil
}
ds.AppConfigFunc = func(_ context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
}
var capturedActivities []*fleet.ActivityTypeRanCustomMDMCommand
var capturedUsers []*activity_api.User
opts.ActivityMock.NewActivityFunc = func(_ context.Context, u *activity_api.User, act activity_api.ActivityDetails) error {
if a, ok := act.(*fleet.ActivityTypeRanCustomMDMCommand); ok {
capturedActivities = append(capturedActivities, a)
capturedUsers = append(capturedUsers, u)
}
return nil
}
rawCmd := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CommandUUID</key>
<string>test-partial-fail-001</string>
<key>Command</key>
<dict>
<key>RequestType</key>
<string>ShutDownDevice</string>
</dict>
</dict>
</plist>`
encoded := base64.StdEncoding.EncodeToString([]byte(rawCmd))
_, err := svc.RunMDMCommand(ctx, encoded, []string{host1.UUID, host2.UUID})
require.NoError(t, err)
require.Len(t, capturedActivities, 1, "expected activity for 1 successful host only")
assert.Equal(t, host1.ID, capturedActivities[0].HostID)
assert.Equal(t, host1.UUID, capturedActivities[0].HostUUID)
assert.Equal(t, "ShutDownDevice", capturedActivities[0].RequestType)
assert.Equal(t, "darwin", capturedActivities[0].Platform)
require.NotNil(t, capturedUsers[0])
assert.Equal(t, test.UserAdmin.ID, capturedUsers[0].ID)
assert.Equal(t, test.UserAdmin.Email, capturedUsers[0].Email)
}
func TestRunMDMCommandSetRecoveryLockBlocked(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)