diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go
index 4870698616..d0f30c1e6e 100644
--- a/cmd/fleetctl/get.go
+++ b/cmd/fleetctl/get.go
@@ -1,7 +1,6 @@
package main
import (
- "bytes"
"encoding/json"
"errors"
"fmt"
@@ -12,6 +11,7 @@ import (
"strconv"
"time"
+ "github.com/beevik/etree"
"github.com/fatih/color"
"github.com/fleetdm/fleet/v4/pkg/rawjson"
"github.com/fleetdm/fleet/v4/pkg/secure"
@@ -1122,6 +1122,15 @@ func printKeyValueTable(c *cli.Context, rows [][]string) {
table.Render()
}
+func printTableWithXML(c *cli.Context, columns []string, data [][]string) {
+ table := defaultTable(c.App.Writer)
+ table.SetHeader(columns)
+ table.SetReflowDuringAutoWrap(false)
+ table.SetAutoWrapText(false)
+ table.AppendBulk(data)
+ table.Render()
+}
+
func getTeamsJSONFlag() cli.Flag {
return &cli.BoolFlag{
Name: jsonFlagName,
@@ -1424,9 +1433,14 @@ func getMDMCommandResultsCommand() *cli.Command {
// print the results as a table
data := [][]string{}
for _, r := range res {
- if bytes.Contains(r.Result, []byte("\t")) {
- // tabs in the XML result tends to break the table formatting
- r.Result = bytes.ReplaceAll(r.Result, []byte("\t"), []byte(" "))
+ formattedResult, err := formatXML(r.Result)
+ // if we get an error, just log it and use the
+ // unformatted command
+ if err != nil {
+ if getDebug(c) {
+ log(c, fmt.Sprintf("error formatting command: %s\n", err))
+ }
+ formattedResult = r.Result
}
data = append(data, []string{
r.CommandUUID,
@@ -1434,11 +1448,11 @@ func getMDMCommandResultsCommand() *cli.Command {
r.RequestType,
r.Status,
r.Hostname,
- string(r.Result),
+ string(formattedResult),
})
}
columns := []string{"ID", "TIME", "TYPE", "STATUS", "HOSTNAME", "RESULTS"}
- printTable(c, columns, data)
+ printTableWithXML(c, columns, data)
return nil
},
@@ -1493,3 +1507,12 @@ func getMDMCommandsCommand() *cli.Command {
},
}
}
+
+func formatXML(in []byte) ([]byte, error) {
+ doc := etree.NewDocument()
+ if err := doc.ReadFromBytes(in); err != nil {
+ return nil, err
+ }
+ doc.Indent(2)
+ return doc.WriteToBytes()
+}
diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go
index 80dc60cbda..877560b07f 100644
--- a/cmd/fleetctl/get_test.go
+++ b/cmd/fleetctl/get_test.go
@@ -2182,33 +2182,43 @@ func TestGetMDMCommandResults(t *testing.T) {
t.Run("command results", func(t *testing.T) {
expectedOutput := strings.TrimSpace(`
-+-----------+----------------------+------+--------------+----------+---------------------------------------------------+
-| ID | TIME | TYPE | STATUS | HOSTNAME | RESULTS |
-+-----------+----------------------+------+--------------+----------+---------------------------------------------------+
-| valid-cmd | 2023-04-04T15:29:00Z | test | Acknowledged | host1 | |
-| | | | | | |
-| | | | | | Command |
-| | | | | | ManagedOnly |
-| | | | | | RequestType |
-| | | | | | ProfileList |
-| | | | | | CommandUUID |
-| | | | | | 0001_ProfileList |
-| | | | | | |
-+-----------+----------------------+------+--------------+----------+---------------------------------------------------+
-| valid-cmd | 2023-04-04T15:29:00Z | test | Error | host2 | |
-| | | | | | |
-| | | | | | Command |
-| | | | | | ManagedOnly |
-| | | | | | RequestType |
-| | | | | | ProfileList |
-| | | | | | CommandUUID |
-| | | | | | 0001_ProfileList |
-| | | | | | |
-+-----------+----------------------+------+--------------+----------+---------------------------------------------------+
++-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
+| ID | TIME | TYPE | STATUS | HOSTNAME | RESULTS |
++-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
+| valid-cmd | 2023-04-04T15:29:00Z | test | Acknowledged | host1 | |
+| | | | | | |
+| | | | | | |
+| | | | | | |
+| | | | | | Command |
+| | | | | | |
+| | | | | | ManagedOnly |
+| | | | | | |
+| | | | | | RequestType |
+| | | | | | ProfileList |
+| | | | | | |
+| | | | | | CommandUUID |
+| | | | | | 0001_ProfileList |
+| | | | | | |
+| | | | | | |
+| | | | | | |
++-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
+| valid-cmd | 2023-04-04T15:29:00Z | test | Error | host2 | |
+| | | | | | |
+| | | | | | |
+| | | | | | |
+| | | | | | Command |
+| | | | | | |
+| | | | | | ManagedOnly |
+| | | | | | |
+| | | | | | RequestType |
+| | | | | | ProfileList |
+| | | | | | |
+| | | | | | CommandUUID |
+| | | | | | 0001_ProfileList |
+| | | | | | |
+| | | | | | |
+| | | | | | |
++-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
`)
platform = "darwin"
@@ -2466,3 +2476,56 @@ func TestGetConfigAgentOptionsSSOAndSMTP(t *testing.T) {
})
}
}
+
+func TestFormatXML(t *testing.T) {
+ tests := []struct {
+ name string
+ input []byte
+ want []byte
+ wantErr bool
+ }{
+ {
+ name: "Basic XML",
+ input: []byte(`content`),
+ want: []byte("\n content\n\n"),
+ wantErr: false,
+ },
+ {
+ name: "Empty XML",
+ input: []byte(""),
+ want: nil,
+ wantErr: false,
+ },
+ {
+ name: "Invalid XML",
+ input: []byte(`contentcontent`),
+ want: []byte("\n content\n\n"),
+ wantErr: false,
+ },
+ {
+ name: "Nested XML",
+ input: []byte(`data`),
+ want: []byte("\n \n data\n \n\n"),
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := formatXML(tt.input)
+
+ if tt.wantErr {
+ require.Error(t, err, "Expected error but got none")
+ } else {
+ require.NoError(t, err, "Unexpected error")
+ require.Equal(t, tt.want, got, "Output XML does not match expected")
+ }
+ })
+ }
+}
diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go
index 85d6f034c0..f2546add00 100644
--- a/server/datastore/mysql/microsoft_mdm.go
+++ b/server/datastore/mysql/microsoft_mdm.go
@@ -347,7 +347,7 @@ SELECT
wmcr.status_code as status,
wmcr.updated_at,
wmc.target_loc_uri as request_type,
- wmcr.raw_result as result
+ wmr.raw_response as result
FROM
windows_mdm_command_results wmcr
INNER JOIN
@@ -358,6 +358,10 @@ INNER JOIN
mdm_windows_enrollments mwe
ON
wmcr.enrollment_id = mwe.id
+INNER JOIN
+ windows_mdm_responses wmr
+ON
+ wmr.id = wmcr.response_id
WHERE
wmcr.command_uuid = ?
`
diff --git a/server/datastore/mysql/microsoft_mdm_test.go b/server/datastore/mysql/microsoft_mdm_test.go
index ed5482a76f..ef1c154704 100644
--- a/server/datastore/mysql/microsoft_mdm_test.go
+++ b/server/datastore/mysql/microsoft_mdm_test.go
@@ -635,7 +635,8 @@ func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) {
_, err = insertDB(t, `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`, cmdUUID, rawCmd, cmdTarget)
require.NoError(t, err)
- responseID, err := insertDB(t, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, "some-response")
+ rawResponse := []byte("some-response")
+ responseID, err := insertDB(t, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, rawResponse)
require.NoError(t, err)
rawResult := []byte("some-result")
@@ -652,7 +653,7 @@ func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) {
require.Len(t, results, 1)
require.Equal(t, dev.HostUUID, results[0].HostUUID)
require.Equal(t, cmdUUID, results[0].CommandUUID)
- require.Equal(t, rawResult, results[0].Result)
+ require.Equal(t, rawResponse, results[0].Result)
require.Equal(t, cmdTarget, results[0].RequestType)
require.Equal(t, statusCode, results[0].Status)
require.Empty(t, results[0].Hostname) // populated only at the service layer
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index d7711dfe4c..99dd0c0291 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -4573,8 +4573,9 @@ func (s *integrationMDMTestSuite) TestMDMWindowsCommandResults() {
})
var responseID int64
+ rawResponse := []byte("some-response")
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
- res, err := q.ExecContext(ctx, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, "some-response")
+ res, err := q.ExecContext(ctx, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, rawResponse)
if err != nil {
return err
}
@@ -4594,7 +4595,7 @@ func (s *integrationMDMTestSuite) TestMDMWindowsCommandResults() {
require.Len(t, resp.Results, 1)
require.Equal(t, dev.HostUUID, resp.Results[0].HostUUID)
require.Equal(t, cmdUUID, resp.Results[0].CommandUUID)
- require.Equal(t, rawResult, resp.Results[0].Result)
+ require.Equal(t, rawResponse, resp.Results[0].Result)
require.Equal(t, cmdTarget, resp.Results[0].RequestType)
require.Equal(t, statusCode, resp.Results[0].Status)
require.Equal(t, h.Hostname, resp.Results[0].Hostname)
@@ -7455,17 +7456,32 @@ func (s *integrationMDMTestSuite) TestWindowsMDM() {
require.Len(t, cmds, 1)
// check command results
+
+ getCommandFullResult := func(cmdUUID string) []byte {
+ var fullResult []byte
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(context.Background(), q, &fullResult, `
+ SELECT raw_response
+ FROM windows_mdm_responses wmr
+ JOIN windows_mdm_command_results wmcr ON wmcr.response_id = wmr.id
+ WHERE command_uuid = ?
+ `, cmdUUID)
+ })
+ return fullResult
+ }
+
var getMDMCmdResp getMDMCommandResultsResponse
s.DoJSON("GET", "/api/latest/fleet/mdm/commandresults", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", cmdOneUUID)
require.Len(t, getMDMCmdResp.Results, 1)
require.NotZero(t, getMDMCmdResp.Results[0].UpdatedAt)
getMDMCmdResp.Results[0].UpdatedAt = time.Time{}
+ fmt.Println(string(getMDMCmdResp.Results[0].Result))
require.Equal(t, &fleet.MDMCommandResult{
HostUUID: orbitHost.UUID,
CommandUUID: cmdOneUUID,
Status: "200",
RequestType: "./Device/Vendor/MSFT/Reboot/RebootNow",
- Result: []byte{},
+ Result: getCommandFullResult(cmdOneUUID),
Hostname: "TestIntegrationsMDM/TestWindowsMDMh1.local",
}, getMDMCmdResp.Results[0])
@@ -7478,7 +7494,7 @@ func (s *integrationMDMTestSuite) TestWindowsMDM() {
CommandUUID: cmdTwoUUID,
Status: "200",
RequestType: "./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID",
- Result: []byte(fmt.Sprintf(`%s1%sReplace200- ./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID0
`, cmdTwoRespUUID, cmdTwoUUID)),
+ Result: getCommandFullResult(cmdTwoUUID),
Hostname: "TestIntegrationsMDM/TestWindowsMDMh1.local",
}, getMDMCmdResp.Results[0])
@@ -7491,7 +7507,7 @@ func (s *integrationMDMTestSuite) TestWindowsMDM() {
CommandUUID: cmdThreeUUID,
Status: "200",
RequestType: "./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID",
- Result: []byte{},
+ Result: getCommandFullResult(cmdThreeUUID),
Hostname: "TestIntegrationsMDM/TestWindowsMDMh1.local",
}, getMDMCmdResp.Results[0])
}