osquery-perf: add support for Windows MDM enrollment and session management. (#17522)

This commit is contained in:
Martin Angers 2024-03-13 09:29:25 -04:00 committed by GitHub
parent ad5c0a90be
commit c358bde87b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 168 additions and 67 deletions

View file

@ -0,0 +1 @@
* Added Windows MDM support to the `osquery-perf` host-simulation command.

View file

@ -8,6 +8,7 @@ import (
"embed"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"flag"
"fmt"
@ -27,6 +28,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
@ -111,6 +113,7 @@ type Stats struct {
osqueryEnrollments int
orbitEnrollments int
mdmEnrollments int
mdmSessions int
distributedWrites int
mdmCommandsReceived int
distributedReads int
@ -152,6 +155,12 @@ func (s *Stats) IncrementMDMEnrollments() {
s.mdmEnrollments++
}
func (s *Stats) IncrementMDMSessions() {
s.l.Lock()
defer s.l.Unlock()
s.mdmSessions++
}
func (s *Stats) IncrementDistributedWrites() {
s.l.Lock()
defer s.l.Unlock()
@ -238,7 +247,7 @@ func (s *Stats) Log() {
defer s.l.Unlock()
log.Printf(
"uptime: %s, error rate: %.2f, osquery enrolls: %d, orbit enrolls: %d, mdm enrolls: %d, distributed/reads: %d, distributed/writes: %d, config requests: %d, result log requests: %d, mdm commands received: %d, config errors: %d, distributed/read errors: %d, distributed/write errors: %d, log result errors: %d, orbit errors: %d, desktop errors: %d, mdm errors: %d, buffered logs: %d",
"uptime: %s, error rate: %.2f, osquery enrolls: %d, orbit enrolls: %d, mdm enrolls: %d, distributed/reads: %d, distributed/writes: %d, config requests: %d, result log requests: %d, mdm sessions initiated: %d, mdm commands received: %d, config errors: %d, distributed/read errors: %d, distributed/write errors: %d, log result errors: %d, orbit errors: %d, desktop errors: %d, mdm errors: %d, buffered logs: %d",
time.Since(s.startTime).Round(time.Second),
float64(s.errors)/float64(s.osqueryEnrollments),
s.osqueryEnrollments,
@ -248,6 +257,7 @@ func (s *Stats) Log() {
s.distributedWrites,
s.configRequests,
s.resultLogRequests,
s.mdmSessions,
s.mdmCommandsReceived,
s.configErrors,
s.distributedReadErrors,
@ -345,8 +355,11 @@ type agent struct {
deviceAuthToken *string
orbitNodeKey *string
// mdmClient simulates a device running the MDM protocol (client side).
mdmClient *mdmtest.TestAppleMDMClient
// macMDMClient and winMDMClient simulate a device running the MDM protocol
// (client side) against Fleet MDM.
macMDMClient *mdmtest.TestAppleMDMClient
winMDMClient *mdmtest.TestWindowsMDMClient
// isEnrolledToMDM is true when the mdmDevice has enrolled.
isEnrolledToMDM bool
// isEnrolledToMDMMu protects isEnrolledToMDM.
@ -428,18 +441,46 @@ func newAgent(
if rand.Float64() <= emptySerialProb {
serialNumber = ""
}
uuid := strings.ToUpper(uuid.New().String())
var mdmClient *mdmtest.TestAppleMDMClient
if rand.Float64() <= mdmProb {
mdmClient = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
SCEPChallenge: mdmSCEPChallenge,
SCEPURL: serverAddress + apple_mdm.SCEPPath,
MDMURL: serverAddress + apple_mdm.MDMPath,
})
// Have the osquery agent match the MDM device serial number and UUID.
serialNumber = mdmClient.SerialNumber
uuid = mdmClient.UUID
hostUUID := strings.ToUpper(uuid.New().String())
// determine the simulated host's OS based on the template name (see
// validTemplateNames below for the list of possible names, the OS is always
// the part before the underscore). Note that it is the OS and not the
// "normalized" platform, so "ubuntu" and not "linux", "macos" and not
// "darwin".
agentOS := strings.TrimRight(templates.Name(), ".tmpl")
agentOS, _, _ = strings.Cut(agentOS, "_")
var (
macMDMClient *mdmtest.TestAppleMDMClient
winMDMClient *mdmtest.TestWindowsMDMClient
)
if rand.Float64() < mdmProb {
switch agentOS {
case "macos":
macMDMClient = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
SCEPChallenge: mdmSCEPChallenge,
SCEPURL: serverAddress + apple_mdm.SCEPPath,
MDMURL: serverAddress + apple_mdm.MDMPath,
})
// Have the osquery agent match the MDM device serial number and UUID.
serialNumber = macMDMClient.SerialNumber
hostUUID = macMDMClient.UUID
case "windows":
// windows MDM enrollment requires orbit enrollment
if deviceAuthToken == nil {
deviceAuthToken = ptr.String(uuid.NewString())
}
// creating the Windows MDM client requires the orbit node key, but we
// only get it after orbit enrollment. So here we just set the value to a
// placeholder (non-nil) client, the actual usable client will be created
// after orbit enrollment, and after receiving the enrollment
// notification.
winMDMClient = new(mdmtest.TestWindowsMDMClient)
}
}
return &agent{
agentIndex: agentIndex,
serverAddress: serverAddress,
@ -453,17 +494,18 @@ func newAgent(
liveQueryNoResultsProb: liveQueryNoResultsProb,
templates: templates,
deviceAuthToken: deviceAuthToken,
os: strings.TrimRight(templates.Name(), ".tmpl"),
os: agentOS,
EnrollSecret: enrollSecret,
ConfigInterval: configInterval,
LogInterval: logInterval,
QueryInterval: queryInterval,
MDMCheckInInterval: mdmCheckInInterval,
UUID: uuid,
UUID: hostUUID,
SerialNumber: serialNumber,
mdmClient: mdmClient,
macMDMClient: macMDMClient,
winMDMClient: winMDMClient,
disableScriptExec: disableScriptExec,
disableFleetDesktop: disableFleetDesktop,
loggerTLSMaxLines: loggerTLSMaxLines,
@ -498,8 +540,15 @@ func (a *agent) isOrbit() bool {
func (a *agent) runLoop(i int, onlyAlreadyEnrolled bool) {
if a.isOrbit() {
if err := a.orbitEnroll(); err != nil {
// clean-up any placeholder mdm client that depended on orbit enrollment
// - there's no concurrency yet for a given agent instance, runLoop is
// the place where the goroutines will be started later on.
a.winMDMClient = nil
return
}
if a.winMDMClient != nil {
a.winMDMClient = mdmtest.NewTestMDMClientWindowsProgramatic(a.serverAddress, *a.orbitNodeKey)
}
}
if err := a.enroll(i, onlyAlreadyEnrolled); err != nil {
@ -519,15 +568,17 @@ func (a *agent) runLoop(i int, onlyAlreadyEnrolled bool) {
go a.runOrbitLoop()
}
if a.mdmClient != nil {
if err := a.mdmClient.Enroll(); err != nil {
log.Printf("MDM enroll failed: %s", err)
// NOTE: the windows MDM client enrollment is only done after receiving a
// notification via the config in the runOrbitLoop.
if a.macMDMClient != nil {
if err := a.macMDMClient.Enroll(); err != nil {
log.Printf("macOS MDM enroll failed: %s", err)
a.stats.IncrementMDMErrors()
return
}
a.setMDMEnrolled()
a.stats.IncrementMDMEnrollments()
go a.runMDMLoop()
go a.runMacosMDMLoop()
}
//
@ -756,6 +807,9 @@ func (a *agent) runOrbitLoop() {
// fleet desktop polls for policy compliance every 5 minutes
fleetDesktopPolicyTicker := time.Tick(5 * time.Minute)
const windowsMDMEnrollmentAttemptFrequency = time.Hour
var lastEnrollAttempt time.Time
for {
select {
case <-orbitConfigTicker:
@ -769,6 +823,20 @@ func (a *agent) runOrbitLoop() {
// that will simulate executing them.
go a.execScripts(cfg.Notifications.PendingScriptExecutionIDs, orbitClient)
}
if cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment &&
!a.mdmEnrolled() &&
a.winMDMClient != nil &&
time.Since(lastEnrollAttempt) > windowsMDMEnrollmentAttemptFrequency {
lastEnrollAttempt = time.Now()
if err := a.winMDMClient.Enroll(); err != nil {
log.Printf("Windows MDM enroll failed: %s", err)
a.stats.IncrementMDMErrors()
} else {
a.setMDMEnrolled()
a.stats.IncrementMDMEnrollments()
go a.runWindowsMDMLoop()
}
}
case <-orbitTokenRemoteCheckTicker:
if !a.disableFleetDesktop && tokenRotationEnabled {
if err := deviceClient.CheckToken(*a.deviceAuthToken); err != nil {
@ -806,20 +874,22 @@ func (a *agent) runOrbitLoop() {
}
}
func (a *agent) runMDMLoop() {
func (a *agent) runMacosMDMLoop() {
mdmCheckInTicker := time.Tick(a.MDMCheckInInterval)
for range mdmCheckInTicker {
mdmCommandPayload, err := a.mdmClient.Idle()
mdmCommandPayload, err := a.macMDMClient.Idle()
if err != nil {
log.Printf("MDM Idle request failed: %s", err)
a.stats.IncrementMDMErrors()
continue
}
a.stats.IncrementMDMSessions()
INNER_FOR_LOOP:
for mdmCommandPayload != nil {
a.stats.IncrementMDMCommandsReceived()
mdmCommandPayload, err = a.mdmClient.Acknowledge(mdmCommandPayload.CommandUUID)
mdmCommandPayload, err = a.macMDMClient.Acknowledge(mdmCommandPayload.CommandUUID)
if err != nil {
log.Printf("MDM Acknowledge request failed: %s", err)
a.stats.IncrementMDMErrors()
@ -829,6 +899,48 @@ func (a *agent) runMDMLoop() {
}
}
func (a *agent) runWindowsMDMLoop() {
mdmCheckInTicker := time.Tick(a.MDMCheckInInterval)
for range mdmCheckInTicker {
cmds, err := a.winMDMClient.StartManagementSession()
if err != nil {
log.Printf("MDM check-in start session request failed: %s", err)
a.stats.IncrementMDMErrors()
continue
}
a.stats.IncrementMDMSessions()
// send a successful ack for each command
msgID, err := a.winMDMClient.GetCurrentMsgID()
if err != nil {
log.Printf("MDM get current MsgID failed: %s", err)
a.stats.IncrementMDMErrors()
continue
}
for _, c := range cmds {
a.stats.IncrementMDMCommandsReceived()
status := syncml.CmdStatusOK
a.winMDMClient.AppendResponse(fleet.SyncMLCmd{
XMLName: xml.Name{Local: fleet.CmdStatus},
MsgRef: &msgID,
CmdRef: &c.Cmd.CmdID.Value,
Cmd: ptr.String(c.Verb),
Data: &status,
Items: nil,
CmdID: fleet.CmdID{Value: uuid.NewString()},
})
}
if _, err := a.winMDMClient.SendResponse(); err != nil {
log.Printf("MDM send response request failed: %s", err)
a.stats.IncrementMDMErrors()
continue
}
}
}
func (a *agent) execScripts(execIDs []string, orbitClient *service.OrbitClient) {
if a.scriptExecRunning.Swap(true) {
// if Swap returns true, the goroutine was already running, exit
@ -1244,21 +1356,6 @@ func (a *agent) randomQueryStats() []map[string]string {
return stats
}
var possibleMDMServerURLs = []string{
"https://kandji.com/1",
"https://jamf.com/1",
"https://airwatch.com/1",
"https://microsoft.com/1",
"https://simplemdm.com/1",
"https://example.com/1",
"https://kandji.com/2",
"https://jamf.com/2",
"https://airwatch.com/2",
"https://microsoft.com/2",
"https://simplemdm.com/2",
"https://example.com/2",
}
// mdmMac returns the results for the `mdm` table query.
//
// If the host is enrolled via MDM it will return installed_from_dep as false
@ -1273,7 +1370,12 @@ func (a *agent) mdmMac() []map[string]string {
}
}
return []map[string]string{
{"enrolled": "true", "server_url": a.mdmClient.EnrollInfo.MDMURL, "installed_from_dep": "false"},
{
"enrolled": "true",
"server_url": a.macMDMClient.EnrollInfo.MDMURL,
"installed_from_dep": "false",
"payload_identifier": apple_mdm.FleetPayloadIdentifier,
},
}
}
@ -1292,26 +1394,20 @@ func (a *agent) setMDMEnrolled() {
}
func (a *agent) mdmWindows() []map[string]string {
autopilot := rand.Intn(2) == 1
ix := rand.Intn(len(possibleMDMServerURLs))
serverURL := possibleMDMServerURLs[ix]
providerID := fleet.MDMNameFromServerURL(serverURL)
installType := "Microsoft Workstation"
if rand.Intn(4) == 1 {
installType = "Microsoft Server"
if !a.mdmEnrolled() {
return []map[string]string{
// empty service url means not enrolled
{"is_federated": "0", "discovery_service_url": "", "provider_id": "", "installation_type": "Client"},
}
}
rows := []map[string]string{
{"key": "discovery_service_url", "value": serverURL},
{"key": "installation_type", "value": installType},
return []map[string]string{
{
"is_federated": "0",
"discovery_service_url": a.serverAddress,
"provider_id": fleet.WellKnownMDMFleet,
"installation_type": "Client",
},
}
if providerID != "" {
rows = append(rows, map[string]string{"key": "provider_id", "value": providerID})
}
if autopilot {
rows = append(rows, map[string]string{"key": "autopilot", "value": "true"})
}
return rows
}
var munkiIssues = func() []string {
@ -1482,15 +1578,19 @@ func (a *agent) processQuery(name, query string) (
case name == hostDetailQueryPrefix+"scheduled_query_stats":
return true, a.randomQueryStats(), &statusOK, nil, nil
case name == hostDetailQueryPrefix+"mdm":
ss := fleet.OsqueryStatus(rand.Intn(2))
if ss == fleet.StatusOK {
ss := statusOK
if rand.Intn(10) > 0 { // 90% success
results = a.mdmMac()
} else {
ss = statusNotOK
}
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"mdm_windows":
ss := fleet.OsqueryStatus(rand.Intn(2))
if ss == fleet.StatusOK {
ss := statusOK
if rand.Intn(10) > 0 { // 90% success
results = a.mdmWindows()
} else {
ss = statusNotOK
}
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"munki_info":
@ -1533,7 +1633,7 @@ func (a *agent) processQuery(name, query string) (
ss := fleet.OsqueryStatus(rand.Intn(2))
if ss == fleet.StatusOK {
switch a.os {
case "ubuntu_22.04":
case "ubuntu":
results = ubuntuSoftware
}
}
@ -1779,7 +1879,7 @@ func main() {
// osquery-perf will send log requests with results only if there are scheduled queries configured AND it's their time to run.
logInterval = flag.Duration("logger_tls_period", 10*time.Second, "Interval for scheduled queries log requests")
queryInterval = flag.Duration("query_interval", 10*time.Second, "Interval for live query requests")
mdmCheckInInterval = flag.Duration("mdm_check_in_interval", 10*time.Second, "Interval for performing MDM check ins")
mdmCheckInInterval = flag.Duration("mdm_check_in_interval", 10*time.Second, "Interval for performing MDM check-ins (applies to both macOS and Windows)")
onlyAlreadyEnrolled = flag.Bool("only_already_enrolled", false, "Only start agents that are already enrolled")
nodeKeyFile = flag.String("node_key_file", "", "File with node keys to use")
@ -1805,8 +1905,8 @@ func main() {
osTemplates = flag.String("os_templates", "macos_14.1.2", fmt.Sprintf("Comma separated list of host OS templates to use and optionally their host count separated by ':' (any of %v, with or without the .tmpl extension)", allowedTemplateNames))
emptySerialProb = flag.Float64("empty_serial_prob", 0.1, "Probability of a host having no serial number [0, 1]")
mdmProb = flag.Float64("mdm_prob", 0.0, "Probability of a host enrolling via MDM (for macOS) [0, 1]")
mdmSCEPChallenge = flag.String("mdm_scep_challenge", "", "SCEP challenge to use when running MDM enroll")
mdmProb = flag.Float64("mdm_prob", 0.0, "Probability of a host enrolling via Fleet MDM (applies for macOS and Windows hosts, implies orbit enrollment on Windows) [0, 1]")
mdmSCEPChallenge = flag.String("mdm_scep_challenge", "", "SCEP challenge to use when running macOS MDM enroll")
liveQueryFailProb = flag.Float64("live_query_fail_prob", 0.0, "Probability of a live query failing execution in the host")
liveQueryNoResultsProb = flag.Float64("live_query_no_results_prob", 0.2, "Probability of a live query returning no results")

View file

@ -174,7 +174,7 @@ var hostDetailQueries = map[string]DetailQuery{
"os_version_windows": {
Query: `
SELECT os.name, r.data as display_version, k.version
FROM
FROM
registry r,
os_version os,
kernel_info k