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]) }