From c358bde87b232d202672781629b7f56e0186a0b9 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 13 Mar 2024 09:29:25 -0400 Subject: [PATCH] osquery-perf: add support for Windows MDM enrollment and session management. (#17522) --- ...20-add-windows-mdm-support-to-osquery-perf | 1 + cmd/osquery-perf/agent.go | 232 +++++++++++++----- server/service/osquery_utils/queries.go | 2 +- 3 files changed, 168 insertions(+), 67 deletions(-) create mode 100644 changes/16120-add-windows-mdm-support-to-osquery-perf diff --git a/changes/16120-add-windows-mdm-support-to-osquery-perf b/changes/16120-add-windows-mdm-support-to-osquery-perf new file mode 100644 index 000000000000..6afc381463e9 --- /dev/null +++ b/changes/16120-add-windows-mdm-support-to-osquery-perf @@ -0,0 +1 @@ +* Added Windows MDM support to the `osquery-perf` host-simulation command. diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index d143a37c7125..0d8d9c77e9a0 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -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" - } - - rows := []map[string]string{ - {"key": "discovery_service_url", "value": serverURL}, - {"key": "installation_type", "value": installType}, - } - if providerID != "" { - rows = append(rows, map[string]string{"key": "provider_id", "value": providerID}) + if !a.mdmEnrolled() { + return []map[string]string{ + // empty service url means not enrolled + {"is_federated": "0", "discovery_service_url": "", "provider_id": "", "installation_type": "Client"}, + } } - if autopilot { - rows = append(rows, map[string]string{"key": "autopilot", "value": "true"}) + return []map[string]string{ + { + "is_federated": "0", + "discovery_service_url": a.serverAddress, + "provider_id": fleet.WellKnownMDMFleet, + "installation_type": "Client", + }, } - 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") diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 3baf7bb09c9a..878bf0ebf62c 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -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