diff --git a/changes/21979-do-wipe-command b/changes/21979-do-wipe-command
new file mode 100644
index 0000000000..63a7efe4f9
--- /dev/null
+++ b/changes/21979-do-wipe-command
@@ -0,0 +1 @@
+* Extended `POST /api/v1/fleet/hosts/:id/wipe` endpoint to allow users to specify the type of remote wipe for windows hosts.
\ No newline at end of file
diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md
index 89a25de3d7..d93a370efd 100644
--- a/docs/REST API/rest-api.md
+++ b/docs/REST API/rest-api.md
@@ -4455,9 +4455,10 @@ To wipe a macOS, iOS, iPadOS, or Windows host, the host must have MDM turned on.
#### Parameters
-| Name | Type | In | Description |
-| ---------- | ----------------- | ---- | ----------------------------------------------------------------------------- |
-| id | integer | path | **Required**. ID of the host to be wiped. |
+| Name | Type | In | Description |
+|----------| ----------------- | ---- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| id | integer | path | **Required**. ID of the host to be wiped. |
+| windows | object | body | Optional metadata used when wiping Windows hosts. The object includes a `wipe_type` property that can be used for specifying what type of remote wipe to perform. Allowed values are `"doWipe"` and `"doWipeProtected"`. |
#### Example
diff --git a/ee/server/service/hosts.go b/ee/server/service/hosts.go
index 6df42cf038..d17f109b8b 100644
--- a/ee/server/service/hosts.go
+++ b/ee/server/service/hosts.go
@@ -225,7 +225,7 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
return svc.enqueueUnlockHostRequest(ctx, host, lockWipe)
}
-func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
+func (svc *Service) WipeHost(ctx context.Context, hostID uint, metadata *fleet.MDMWipeMetadata) error {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
@@ -317,7 +317,7 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
}
// all good, go ahead with queuing the wipe request.
- return svc.enqueueWipeHostRequest(ctx, host, lockWipe)
+ return svc.enqueueWipeHostRequest(ctx, host, lockWipe, metadata)
}
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus, viewPIN bool) (
@@ -436,7 +436,12 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
return unlockPIN, nil
}
-func (svc *Service) enqueueWipeHostRequest(ctx context.Context, host *fleet.Host, wipeStatus *fleet.HostLockWipeStatus) error {
+func (svc *Service) enqueueWipeHostRequest(
+ ctx context.Context,
+ host *fleet.Host,
+ wipeStatus *fleet.HostLockWipeStatus,
+ metadata *fleet.MDMWipeMetadata,
+) error {
vc, ok := viewer.FromContext(ctx)
if !ok {
return fleet.ErrNoContext
@@ -450,11 +455,16 @@ func (svc *Service) enqueueWipeHostRequest(ctx context.Context, host *fleet.Host
}
case "windows":
+ // default wipe type
+ wipeType := fleet.MDMWindowsWipeTypeDoWipeProtected
+ if metadata != nil && metadata.Windows != nil {
+ wipeType = metadata.Windows.WipeType
+ }
wipeCmdUUID := uuid.NewString()
wipeCmd := &fleet.MDMWindowsCommand{
CommandUUID: wipeCmdUUID,
- RawCommand: []byte(fmt.Sprintf(windowsWipeCommand, wipeCmdUUID)),
- TargetLocURI: "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected",
+ RawCommand: []byte(fmt.Sprintf(windowsWipeCommand, wipeCmdUUID, wipeType.String())),
+ TargetLocURI: fmt.Sprintf("./Device/Vendor/MSFT/RemoteWipe/%s", wipeType.String()),
}
if err := svc.ds.WipeHostViaWindowsMDM(ctx, host, wipeCmd); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing wipe request for windows")
@@ -506,7 +516,7 @@ var (
%s
-
- ./Device/Vendor/MSFT/RemoteWipe/doWipeProtected
+ ./Device/Vendor/MSFT/RemoteWipe/%s
chr
diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go
index 423f23b0a1..1a6e3ebc6d 100644
--- a/server/fleet/mdm.go
+++ b/server/fleet/mdm.go
@@ -1005,3 +1005,8 @@ type MDMConfigProfileStatus struct {
Pending uint `json:"pending" db:"pending"`
Failed uint `json:"failed" db:"failed"`
}
+
+// MDMWipeMetadata specifies optional metadata for the remote wipe command
+type MDMWipeMetadata struct {
+ Windows *MDMWindowsWipeMetadata
+}
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 084192949a..956ebfe747 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -1186,7 +1186,7 @@ type Service interface {
// Script-based methods (at least for some platforms, MDM-based for others)
LockHost(ctx context.Context, hostID uint, viewPIN bool) (unlockPIN string, err error)
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
- WipeHost(ctx context.Context, hostID uint) error
+ WipeHost(ctx context.Context, hostID uint, metadata *MDMWipeMetadata) error
///////////////////////////////////////////////////////////////////////////////
// Software installers
diff --git a/server/fleet/windows_mdm.go b/server/fleet/windows_mdm.go
index 5ce597e8d9..d28dd50822 100644
--- a/server/fleet/windows_mdm.go
+++ b/server/fleet/windows_mdm.go
@@ -2,6 +2,7 @@ package fleet
import (
"bytes"
+ "encoding/json"
"encoding/xml"
"errors"
"fmt"
@@ -211,3 +212,42 @@ type MDMWindowsProfileContents struct {
SyncML []byte `db:"syncml"`
Checksum []byte `db:"checksum"`
}
+
+// MDMWindowsWipeType specifies what type of remote wipe we want
+// to perform.
+type MDMWindowsWipeType int
+
+const (
+ MDMWindowsWipeTypeDoWipe MDMWindowsWipeType = iota
+ MDMWindowsWipeTypeDoWipeProtected
+)
+
+var wipeTypeVariants = map[MDMWindowsWipeType]string{
+ MDMWindowsWipeTypeDoWipe: "doWipe",
+ MDMWindowsWipeTypeDoWipeProtected: "doWipeProtected",
+}
+
+func (wt *MDMWindowsWipeType) String() string {
+ if wt == nil {
+ return ""
+ }
+ return wipeTypeVariants[*wt]
+}
+
+func (wt *MDMWindowsWipeType) UnmarshalJSON(b []byte) error {
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+ for k, v := range wipeTypeVariants {
+ if v == s {
+ *wt = k
+ return nil
+ }
+ }
+ return fmt.Errorf("invalid WipeType: %s", s)
+}
+
+type MDMWindowsWipeMetadata struct {
+ WipeType MDMWindowsWipeType `json:"wipe_type"`
+}
diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go
index b0a7e3d558..14e2e55895 100644
--- a/server/service/hosts_test.go
+++ b/server/service/hosts_test.go
@@ -2002,9 +2002,9 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
return &fleet.HostLockWipeStatus{}, nil
}
- err = svc.WipeHost(ctx, globalHostID)
+ err = svc.WipeHost(ctx, globalHostID, nil)
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
- err = svc.WipeHost(ctx, teamHostID)
+ err = svc.WipeHost(ctx, teamHostID, nil)
checkAuthErr(t, tt.shouldFailTeamWrite, err)
})
}
diff --git a/server/service/integration_mdm_lifecycle_test.go b/server/service/integration_mdm_lifecycle_test.go
index cb0db41754..1d9292f660 100644
--- a/server/service/integration_mdm_lifecycle_test.go
+++ b/server/service/integration_mdm_lifecycle_test.go
@@ -289,7 +289,7 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
s.Do(
"POST",
fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID),
- nil,
+ json.RawMessage(`{ "windows": {"wipe_type": "doWipe"}}`),
http.StatusOK,
)
@@ -305,7 +305,7 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
require.NotNil(t, wipeCmd)
require.Equal(t, wipeCmd.Verb, fleet.CmdExec)
require.Len(t, wipeCmd.Cmd.Items, 1)
- require.EqualValues(t, "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected", *wipeCmd.Cmd.Items[0].Target)
+ require.EqualValues(t, "./Device/Vendor/MSFT/RemoteWipe/doWipe", *wipeCmd.Cmd.Items[0].Target)
msgID, err := device.GetCurrentMsgID()
require.NoError(t, err)
diff --git a/server/service/scripts.go b/server/service/scripts.go
index 68c9228fa1..a86e27c2cb 100644
--- a/server/service/scripts.go
+++ b/server/service/scripts.go
@@ -2,11 +2,14 @@ package service
import (
"context"
+ "crypto/x509"
+ "encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
+ "net/url"
"path/filepath"
"strconv"
"time"
@@ -1305,12 +1308,35 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
return "", fleet.ErrMissingLicense
}
-////////////////////////////////////////////////////////////////////////////////
+// //////////////////////////////////////////////////////////////////////////////
// Wipe host
-////////////////////////////////////////////////////////////////////////////////
+// //////////////////////////////////////////////////////////////////////////////
+
+func (req *wipeHostRequest) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error {
+ if r == nil {
+ return nil
+ }
+
+ decoder := json.NewDecoder(io.LimitReader(r, 100*1024))
+ metadata := fleet.MDMWipeMetadata{}
+ if err := decoder.Decode(&metadata); err != nil {
+ if err == io.EOF {
+ // OK ... body is optional
+ return nil
+ }
+ return &fleet.BadRequestError{
+ Message: "failed to unmarshal request body",
+ InternalErr: err,
+ }
+ }
+ req.Metadata = &metadata
+
+ return nil
+}
type wipeHostRequest struct {
- HostID uint `url:"id"`
+ HostID uint `url:"id"`
+ Metadata *fleet.MDMWipeMetadata
}
type wipeHostResponse struct {
@@ -1323,14 +1349,14 @@ func (r wipeHostResponse) Error() error { return r.Err }
func wipeHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*wipeHostRequest)
- if err := svc.WipeHost(ctx, req.HostID); err != nil {
+ if err := svc.WipeHost(ctx, req.HostID, req.Metadata); err != nil {
return wipeHostResponse{Err: err}, nil
}
// We bail if a host is locked or wiped, so we can assume the host is unlocked at this point
return wipeHostResponse{DeviceStatus: fleet.DeviceStatusUnlocked, PendingAction: fleet.PendingActionWipe}, nil
}
-func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
+func (svc *Service) WipeHost(ctx context.Context, _ uint, _ *fleet.MDMWipeMetadata) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
diff --git a/server/service/scripts_test.go b/server/service/scripts_test.go
index 3fae585ce3..54d529399d 100644
--- a/server/service/scripts_test.go
+++ b/server/service/scripts_test.go
@@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
+ "io"
"strings"
"testing"
"time"
@@ -968,3 +969,80 @@ func TestBatchScriptExecute(t *testing.T) {
require.Equal(t, []uint{3, 4}, requestedHostIds)
})
}
+
+func TestWipeHostRequestDecodeBody(t *testing.T) {
+ ctx := context.Background()
+
+ testCases := []struct {
+ name string
+ body io.Reader
+ expectedError string
+ expectation func(t *testing.T, req *wipeHostRequest)
+ }{
+ {
+ name: "empty body",
+ body: strings.NewReader(""),
+ expectation: func(t *testing.T, req *wipeHostRequest) {
+ require.Nil(t, req.Metadata)
+ },
+ },
+ {
+ name: "doWipe",
+ body: strings.NewReader(`{"windows": {"wipe_type": "doWipe"}}`),
+ expectation: func(t *testing.T, req *wipeHostRequest) {
+ require.NotNil(t, req.Metadata)
+ require.NotNil(t, req.Metadata.Windows)
+ require.Equal(t, fleet.MDMWindowsWipeTypeDoWipe, req.Metadata.Windows.WipeType)
+ },
+ },
+ {
+ name: "doWipeProtected",
+ body: strings.NewReader(`{"windows": {"wipe_type": "doWipeProtected"}}`),
+ expectation: func(t *testing.T, req *wipeHostRequest) {
+ require.NotNil(t, req.Metadata)
+ require.NotNil(t, req.Metadata.Windows)
+ require.Equal(t, fleet.MDMWindowsWipeTypeDoWipeProtected, req.Metadata.Windows.WipeType)
+ },
+ },
+ {
+ name: "invalid wipe type",
+ body: strings.NewReader(`{"windows": {"wipe_type": "doWipeProtectedII"}}`),
+ expectedError: "failed to unmarshal request body",
+ },
+ {
+ name: "empty payload",
+ body: strings.NewReader(`{}`),
+ expectation: func(t *testing.T, req *wipeHostRequest) {
+ require.NotNil(t, req.Metadata)
+ require.Nil(t, req.Metadata.Windows)
+ },
+ },
+ {
+ name: "windows field is null",
+ body: strings.NewReader(`{"windows": null}`),
+ expectation: func(t *testing.T, req *wipeHostRequest) {
+ require.NotNil(t, req.Metadata)
+ require.Nil(t, req.Metadata.Windows)
+ },
+ },
+ {
+ name: "empty wipe type",
+ body: strings.NewReader(`{"windows": {"wipe_type": null}}`),
+ expectedError: "failed to unmarshal request body",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ sut := wipeHostRequest{}
+ err := sut.DecodeBody(ctx, tc.body, nil, nil)
+
+ if tc.expectedError != "" {
+ require.ErrorContains(t, err, tc.expectedError)
+ } else {
+ require.NoError(t, err)
+ tc.expectation(t, &sut)
+ }
+ })
+ }
+}