mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
374 lines
14 KiB
Go
374 lines
14 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/micromdm/nanodep/godep"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIngestMDMAppleDevicesFromDEPSync(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
ctx := context.Background()
|
|
createBuiltinLabels(t, ds)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
_, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: fmt.Sprintf("hostname_%d", i),
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now().Add(-time.Duration(i) * time.Minute),
|
|
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-host-id_%d", i)),
|
|
NodeKey: ptr.String(fmt.Sprintf("node-key_%d", i)),
|
|
UUID: fmt.Sprintf("uuid_%d", i),
|
|
HardwareSerial: fmt.Sprintf("serial_%d", i),
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 10)
|
|
wantSerials := []string{}
|
|
for _, h := range hosts {
|
|
wantSerials = append(wantSerials, h.HardwareSerial)
|
|
}
|
|
|
|
// mock results incoming from depsync.Syncer
|
|
depDevices := []godep.Device{
|
|
{SerialNumber: "abc", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // ingested; new serial, macOS, "added" op type
|
|
{SerialNumber: "abc", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // not ingested; duplicate serial
|
|
{SerialNumber: hosts[0].HardwareSerial, Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // not ingested; existing serial
|
|
{SerialNumber: "ijk", Model: "IPad Pro", OS: "iOS", OpType: "added"}, // not ingested; iOS
|
|
{SerialNumber: "tuv", Model: "Apple TV", OS: "tvOS", OpType: "added"}, // not ingested; tvOS
|
|
{SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "updated"}, // not ingested; op type "updated"
|
|
{SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "deleted"}, // not ingested; op type "deleted"
|
|
{SerialNumber: "xyz", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, // ingested; new serial, macOS, "added" op type
|
|
}
|
|
wantSerials = append(wantSerials, "abc", "xyz")
|
|
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), n) // 2 new hosts ("abc", "xyz")
|
|
|
|
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, len(wantSerials))
|
|
gotSerials := []string{}
|
|
for _, h := range hosts {
|
|
gotSerials = append(gotSerials, h.HardwareSerial)
|
|
if hs := h.HardwareSerial; hs == "abc" || hs == "xyz" {
|
|
checkMDMHostRelatedTables(t, ds, h.ID, hs, "MacBook Pro")
|
|
}
|
|
}
|
|
require.ElementsMatch(t, wantSerials, gotSerials)
|
|
}
|
|
|
|
func TestHandleMDMCheckinRequest(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, ds *Datastore)
|
|
}{
|
|
{"TestHostAlreadyExistsInFleet", testIngestMDMAppleHostAlreadyExistsInFleet},
|
|
{"TestCheckinAfterDEPSync", testIngestMDMAppleCheckinAfterDEPSync},
|
|
{"TestBeforeDEPSync", testIngestMDMAppleCheckinBeforeDEPSync},
|
|
{"TestMultipleAuthenticateRequests", testIngestMDMAppleCheckinMultipleAuthenticateRequests},
|
|
{"TestCheckOutRequests", testIngestMDMAppleCheckinCheckoutRequests},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer TruncateTables(t, ds)
|
|
createBuiltinLabels(t, ds)
|
|
|
|
c.fn(t, ds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testIngestMDMAppleHostAlreadyExistsInFleet(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
testSerial := "test-serial"
|
|
testUUID := "test-uuid"
|
|
|
|
host, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: "test-host-name",
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now(),
|
|
OsqueryHostID: ptr.String("1337"),
|
|
NodeKey: ptr.String("1337"),
|
|
UUID: testUUID,
|
|
HardwareSerial: testSerial,
|
|
})
|
|
require.NoError(t, err)
|
|
err = ds.SetOrUpdateMDMData(ctx, host.ID, false, false, "https://fleetdm.com", true, "Fleet MDM")
|
|
require.NoError(t, err)
|
|
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
|
|
err = fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("Authenticate", testSerial, testUUID, "MacBook Pro"))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
|
|
// an activity is created
|
|
activities, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 1)
|
|
require.Empty(t, activities[0].ActorID)
|
|
require.JSONEq(
|
|
t,
|
|
`{"host_serial":"test-serial", "installed_from_dep":true}`,
|
|
string(*activities[0].Details),
|
|
)
|
|
}
|
|
|
|
func testIngestMDMAppleCheckinAfterDEPSync(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
testSerial := "test-serial"
|
|
testUUID := "test-uuid"
|
|
|
|
// simulate a host that is first ingested via DEP (e.g., the device was added via Apple Business Manager)
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
|
{SerialNumber: testSerial, Model: "MacBook Pro", OS: "OSX", OpType: "added"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), n)
|
|
|
|
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
// hosts that are first ingested via DEP will have a serial number but not a UUID because UUID
|
|
// is not available from the DEP sync endpoint
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, "", hosts[0].UUID)
|
|
checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, "MacBook Pro")
|
|
|
|
// now simulate the initial MDM checkin by that same host
|
|
err = fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("Authenticate", testSerial, testUUID, "MacBook Pro"))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, "MacBook Pro")
|
|
|
|
// an activity is created
|
|
activities, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 1)
|
|
require.Empty(t, activities[0].ActorID)
|
|
require.JSONEq(
|
|
t,
|
|
`{"host_serial":"test-serial", "installed_from_dep":true}`,
|
|
string(*activities[0].Details),
|
|
)
|
|
}
|
|
|
|
func testIngestMDMAppleCheckinBeforeDEPSync(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
testSerial := "test-serial"
|
|
testUUID := "test-uuid"
|
|
|
|
// ingest host on initial mdm checkin
|
|
err := fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("Authenticate", testSerial, testUUID, "MacBook Pro"))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
|
|
// an activity is created
|
|
activities, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 1)
|
|
require.Empty(t, activities[0].ActorID)
|
|
require.JSONEq(
|
|
t,
|
|
`{"host_serial":"test-serial", "installed_from_dep":false}`,
|
|
string(*activities[0].Details),
|
|
)
|
|
|
|
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, "MacBook Pro")
|
|
|
|
// no effect if same host appears in DEP sync
|
|
n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{
|
|
{SerialNumber: testSerial, Model: "MacBook Pro", OS: "OSX", OpType: "added"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), n)
|
|
|
|
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, "MacBook Pro")
|
|
}
|
|
|
|
func testIngestMDMAppleCheckinMultipleAuthenticateRequests(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
testSerial := "test-serial"
|
|
testUUID := "test-uuid"
|
|
|
|
err := fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("Authenticate", testSerial, testUUID, "MacBook Pro"))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
hosts := listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
|
|
// duplicate Authenticate request has no effect
|
|
err = fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("Authenticate", testSerial, testUUID, "MacBook Pro"))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{}, 1)
|
|
require.Equal(t, testSerial, hosts[0].HardwareSerial)
|
|
require.Equal(t, testUUID, hosts[0].UUID)
|
|
}
|
|
|
|
func testIngestMDMAppleCheckinCheckoutRequests(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
testSerial := "test-serial"
|
|
testUUID := "test-uuid"
|
|
err := fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("Authenticate", testSerial, testUUID, "MacBook Pro"))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
|
|
// CheckOut request updates MDM data and adds an activity
|
|
err = fleet.HandleMDMCheckinRequest(ctx, &http.Request{
|
|
Header: map[string][]string{
|
|
"Content-Type": {"application/x-apple-aspen-mdm-checkin"},
|
|
},
|
|
Method: http.MethodPost,
|
|
Body: io.NopCloser(strings.NewReader(xmlForTest("CheckOut", "", testUUID, ""))),
|
|
}, ds)
|
|
require.NoError(t, err)
|
|
|
|
activities, err := ds.ListActivities(context.Background(), fleet.ListActivitiesOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 2)
|
|
require.Equal(t, "mdm_unenrolled", activities[1].Type)
|
|
require.Empty(t, activities[1].ActorID)
|
|
require.JSONEq(
|
|
t,
|
|
`{"host_serial":"test-serial", "installed_from_dep":false}`,
|
|
string(*activities[1].Details),
|
|
)
|
|
}
|
|
|
|
// checkMDMHostRelatedTables checks that rows are inserted for new MDM hosts in each of
|
|
// host_display_names, host_seen_times, and label_membership. Note that related tables records for
|
|
// pre-existing hosts are created outside of the MDM enrollment flows so they are not checked in
|
|
// some tests above (e.g., testIngestMDMAppleHostAlreadyExistsInFleet)
|
|
func checkMDMHostRelatedTables(t *testing.T, ds *Datastore, hostID uint, expectedSerial string, expectedModel string) {
|
|
var displayName string
|
|
err := sqlx.GetContext(context.Background(), ds.reader, &displayName, `SELECT display_name FROM host_display_names WHERE host_id = ?`, hostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fmt.Sprintf("%s (%s)", expectedModel, expectedSerial), displayName)
|
|
|
|
var labelsOK []bool
|
|
err = sqlx.SelectContext(context.Background(), ds.reader, &labelsOK, `SELECT 1 FROM label_membership WHERE host_id = ?`, hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, labelsOK, 2)
|
|
require.True(t, labelsOK[0])
|
|
require.True(t, labelsOK[1])
|
|
|
|
appCfg, err := ds.AppConfig(context.Background())
|
|
require.NoError(t, err)
|
|
var hmdm fleet.HostMDM
|
|
err = sqlx.GetContext(context.Background(), ds.reader, &hmdm, `SELECT host_id, server_url, mdm_id FROM host_mdm WHERE host_id = ?`, hostID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, hostID, hmdm.HostID)
|
|
require.Equal(t, appCfg.ServerSettings.ServerURL, hmdm.ServerURL)
|
|
require.NotEmpty(t, hmdm.MDMID)
|
|
|
|
var mdmSolution fleet.MDMSolution
|
|
err = sqlx.GetContext(context.Background(), ds.reader, &mdmSolution, `SELECT name, server_url FROM mobile_device_management_solutions WHERE id = ?`, hmdm.MDMID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.WellKnownMDMFleet, mdmSolution.Name)
|
|
require.Equal(t, appCfg.ServerSettings.ServerURL, mdmSolution.ServerURL)
|
|
}
|
|
|
|
func xmlForTest(msgType string, serial string, udid string, model string) string {
|
|
return fmt.Sprintf(`
|
|
<?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>MessageType</key>
|
|
<string>%s</string>
|
|
<key>SerialNumber</key>
|
|
<string>%s</string>
|
|
<key>UDID</key>
|
|
<string>%s</string>
|
|
<key>Model</key>
|
|
<string>%s</string>
|
|
</dict>
|
|
</plist>`, msgType, serial, udid, model)
|
|
}
|
|
|
|
// createBuiltinLabels creates entries for "All Hosts" and "macOS" labels, which are assumed to be
|
|
// extant for MDM flows
|
|
func createBuiltinLabels(t *testing.T, ds *Datastore) {
|
|
_, err := ds.writer.Exec(`
|
|
INSERT INTO labels (
|
|
name,
|
|
description,
|
|
query,
|
|
platform,
|
|
label_type
|
|
) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`,
|
|
"All Hosts",
|
|
"",
|
|
"",
|
|
"",
|
|
fleet.LabelTypeBuiltIn,
|
|
"macOS",
|
|
"",
|
|
"",
|
|
"",
|
|
fleet.LabelTypeBuiltIn,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|