mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
show full formatted results for windows commands in fleetctl (#14922)
for #14912 this adds the full results to the "RESULTS" column of `fleetctl get mdm-command-results`. Additionally I included formatting of the XML output to improve readability.
This commit is contained in:
parent
91db043094
commit
33db665d63
@ -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()
|
||||
}
|
||||
|
@ -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 | <?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>Command</key> <dict> |
|
||||
| | | | | | <key>ManagedOnly</key> <false/> |
|
||||
| | | | | | <key>RequestType</key> |
|
||||
| | | | | | <string>ProfileList</string> |
|
||||
| | | | | | </dict> <key>CommandUUID</key> |
|
||||
| | | | | | <string>0001_ProfileList</string> </dict> |
|
||||
| | | | | | </plist> |
|
||||
+-----------+----------------------+------+--------------+----------+---------------------------------------------------+
|
||||
| valid-cmd | 2023-04-04T15:29:00Z | test | Error | host2 | <?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>Command</key> <dict> |
|
||||
| | | | | | <key>ManagedOnly</key> <false/> |
|
||||
| | | | | | <key>RequestType</key> |
|
||||
| | | | | | <string>ProfileList</string> |
|
||||
| | | | | | </dict> <key>CommandUUID</key> |
|
||||
| | | | | | <string>0001_ProfileList</string> </dict> |
|
||||
| | | | | | </plist> |
|
||||
+-----------+----------------------+------+--------------+----------+---------------------------------------------------+
|
||||
+-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
|
||||
| ID | TIME | TYPE | STATUS | HOSTNAME | RESULTS |
|
||||
+-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
|
||||
| valid-cmd | 2023-04-04T15:29:00Z | test | Acknowledged | host1 | <?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>Command</key> |
|
||||
| | | | | | <dict> |
|
||||
| | | | | | <key>ManagedOnly</key> |
|
||||
| | | | | | <false/> |
|
||||
| | | | | | <key>RequestType</key> |
|
||||
| | | | | | <string>ProfileList</string> |
|
||||
| | | | | | </dict> |
|
||||
| | | | | | <key>CommandUUID</key> |
|
||||
| | | | | | <string>0001_ProfileList</string> |
|
||||
| | | | | | </dict> |
|
||||
| | | | | | </plist> |
|
||||
| | | | | | |
|
||||
+-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
|
||||
| valid-cmd | 2023-04-04T15:29:00Z | test | Error | host2 | <?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>Command</key> |
|
||||
| | | | | | <dict> |
|
||||
| | | | | | <key>ManagedOnly</key> |
|
||||
| | | | | | <false/> |
|
||||
| | | | | | <key>RequestType</key> |
|
||||
| | | | | | <string>ProfileList</string> |
|
||||
| | | | | | </dict> |
|
||||
| | | | | | <key>CommandUUID</key> |
|
||||
| | | | | | <string>0001_ProfileList</string> |
|
||||
| | | | | | </dict> |
|
||||
| | | | | | </plist> |
|
||||
| | | | | | |
|
||||
+-----------+----------------------+------+--------------+----------+--------------------------------------------------------------------------------------------------------+
|
||||
`)
|
||||
|
||||
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(`<root><element>content</element></root>`),
|
||||
want: []byte("<root>\n <element>content</element>\n</root>\n"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty XML",
|
||||
input: []byte(""),
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid XML",
|
||||
input: []byte(`<root><element>content</root`),
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "XML With Attributes",
|
||||
input: []byte(`<root attr="value"><element key="val">content</element></root>`),
|
||||
want: []byte("<root attr=\"value\">\n <element key=\"val\">content</element>\n</root>\n"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Nested XML",
|
||||
input: []byte(`<root><parent><child>data</child></parent></root>`),
|
||||
want: []byte("<root>\n <parent>\n <child>data</child>\n </parent>\n</root>\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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 = ?
|
||||
`
|
||||
|
@ -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
|
||||
|
@ -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(`<Results xmlns="SYNCML:SYNCML1.2"><CmdID>%s</CmdID><MsgRef>1</MsgRef><CmdRef>%s</CmdRef><Cmd>Replace</Cmd><Data>200</Data><Item><Source><LocURI>./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID</LocURI></Source><Data>0</Data></Item></Results>`, 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])
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user