Trigger Windows MDM host enrollment on device when notified that it is enabled (#12426)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added execution of programmatic Windows MDM enrollment on eligible devices when Windows MDM is enabled.
|
||||
@@ -614,10 +614,14 @@ func main() {
|
||||
|
||||
// create the notifications middleware that wraps the orbit client
|
||||
// (must be shared by all runners that use a ConfigFetcher).
|
||||
const renewEnrollmentProfileCommandFrequency = time.Hour
|
||||
const (
|
||||
renewEnrollmentProfileCommandFrequency = time.Hour
|
||||
microsoftMDMEnrollmentCommandFrequency = time.Hour
|
||||
)
|
||||
configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency)
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
// add middleware to handle nudge installation and updates
|
||||
const nudgeLaunchInterval = 30 * time.Minute
|
||||
configFetcher = update.ApplyNudgeConfigFetcherMiddleware(configFetcher, update.NudgeConfigFetcherOptions{
|
||||
@@ -626,6 +630,8 @@ func main() {
|
||||
|
||||
configFetcher = update.ApplyDiskEncryptionRunnerMiddleware(configFetcher)
|
||||
configFetcher = update.ApplySwiftDialogDownloaderMiddleware(configFetcher, updateRunner)
|
||||
case "windows":
|
||||
configFetcher = update.ApplyMicrosoftMDMEnrollmentFetcherMiddleware(configFetcher, microsoftMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID)
|
||||
}
|
||||
|
||||
const orbitFlagsUpdateInterval = 30 * time.Second
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package update
|
||||
|
||||
// Exported so that it can be used in tools/ (so that it can be built for
|
||||
// Windows and tested on a Windows machine). Otherwise not meant to be used
|
||||
// from outside this package.
|
||||
type MicrosoftMDMEnrollmentArgs struct {
|
||||
DiscoveryURL string
|
||||
HostUUID string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package update
|
||||
|
||||
func RunMicrosoftMDMEnrollment(args MicrosoftMDMEnrollmentArgs) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//go:build windows
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
dllMDMRegistration *windows.LazyDLL = windows.NewLazySystemDLL("mdmregistration.dll")
|
||||
|
||||
// RegisterDeviceWithManagement registers a device with a MDM service:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/mdmregistration/nf-mdmregistration-registerdevicewithmanagement
|
||||
procRegisterDeviceWithManagement *windows.LazyProc = dllMDMRegistration.NewProc("RegisterDeviceWithManagement")
|
||||
)
|
||||
|
||||
// Exported so that it can be used in tools/ (so that it can be built for
|
||||
// Windows and tested on a Windows machine). Otherwise not meant to be called
|
||||
// from outside this package.
|
||||
func RunMicrosoftMDMEnrollment(args MicrosoftMDMEnrollmentArgs) error {
|
||||
installType, err := readInstallationType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ToLower(installType) == "server" {
|
||||
// do not enroll, it is a server
|
||||
return errIsWindowsServer
|
||||
}
|
||||
return enrollHostToMDM(args)
|
||||
}
|
||||
|
||||
func readInstallationType() (string, error) {
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
s, _, err := k.GetStringValue("InstallationType")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// TODO(mna): refactor to a Windows-specific package to constrain usage of
|
||||
// unsafe to that package, once https://github.com/fleetdm/fleet/pull/12387
|
||||
// lands.
|
||||
|
||||
// Perform the host MDM enrollment process using MS-MDE protocol:
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde/5c841535-042e-489e-913c-9d783d741267
|
||||
func enrollHostToMDM(args MicrosoftMDMEnrollmentArgs) error {
|
||||
discoveryURLPtr, err := syscall.UTF16PtrFromString(args.DiscoveryURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("discovery URL to UTF16 pointer: %w", err)
|
||||
}
|
||||
|
||||
// we use an empty user UPN, it is not a required argument
|
||||
userPtr, err := syscall.UTF16PtrFromString("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("user UPN to UTF16 pointer: %w", err)
|
||||
}
|
||||
|
||||
accessTok, err := generateWindowsMDMAccessTokenPayload(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate access token payload: %w", err)
|
||||
}
|
||||
accessTokPtr, err := syscall.UTF16PtrFromString(string(accessTok))
|
||||
if err != nil {
|
||||
return fmt.Errorf("access token to UTF16 pointer: %w", err)
|
||||
}
|
||||
|
||||
// pre-load the DLL and pre-find the procedure, to return a more meaningful
|
||||
// message if those steps fail and avoid a panic.
|
||||
if err := dllMDMRegistration.Load(); err != nil {
|
||||
return fmt.Errorf("load MDM dll: %w", err)
|
||||
}
|
||||
if err := procRegisterDeviceWithManagement.Find(); err != nil {
|
||||
return fmt.Errorf("find MDM RegisterDeviceWithManagement procedure: %w", err)
|
||||
}
|
||||
|
||||
code, _, err := procRegisterDeviceWithManagement.Call(
|
||||
uintptr(unsafe.Pointer(userPtr)),
|
||||
uintptr(unsafe.Pointer(discoveryURLPtr)),
|
||||
uintptr(unsafe.Pointer(accessTokPtr)),
|
||||
)
|
||||
log.Debug().Msgf("RegisterDeviceWithManagement returned code: %#x ; message: %v", code, err)
|
||||
if code != uintptr(windows.ERROR_SUCCESS) {
|
||||
// hexadecimal error code can help identify error here:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/mdmreg/mdm-registration-constants
|
||||
// decimal error code can help identify error here (look for the ERROR_xxx constants):
|
||||
// https://pkg.go.dev/golang.org/x/sys/windows#pkg-constants
|
||||
//
|
||||
// Note that the error message may be "The operation completed
|
||||
// successfully." even though there is an error (e.g. if the discovery URL
|
||||
// results in a 404 not found, the error code will be 0x80190194 which
|
||||
// means windows.HTTP_E_STATUS_NOT_FOUND). In this case, translate the
|
||||
// message to something more useful.
|
||||
if httpCode := code - uintptr(windows.HTTP_E_STATUS_BAD_REQUEST); httpCode >= 0 && httpCode < 200 {
|
||||
// status bad request is 400, so if error code is between 400 and < 600.
|
||||
err = fmt.Errorf("using discovery URL %q: HTTP error code %d", args.DiscoveryURL, http.StatusBadRequest+httpCode)
|
||||
}
|
||||
return fmt.Errorf("RegisterDeviceWithManagement failed: %s (%#x - %[2]d)", err, code)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateWindowsMDMAccessTokenPayload(args MicrosoftMDMEnrollmentArgs) ([]byte, error) {
|
||||
var pld fleet.MicrosoftMDMAccessTokenPayload
|
||||
|
||||
pld.Type = fleet.MicrosoftMDMProgrammaticEnrollmentType // always programmatic for now
|
||||
pld.Payload.HostUUID = args.HostUUID
|
||||
return json.Marshal(pld)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
|
||||
type runCmdFunc func() error
|
||||
|
||||
// RenewEnrollmentProfileConfigFetcher is a kind of middleware that wraps an
|
||||
// renewEnrollmentProfileConfigFetcher is a kind of middleware that wraps an
|
||||
// OrbitConfigFetcher and detects if the fleet server sent a notification to
|
||||
// renew the enrollment profile. If so, it runs the command (as root) to
|
||||
// bootstrap the renewal of the profile on the device (the user still needs to
|
||||
@@ -18,7 +19,7 @@ type runCmdFunc func() error
|
||||
//
|
||||
// It ensures only one renewal command is executed at any given time, and that
|
||||
// it doesn't re-execute the command until a certain amount of time has passed.
|
||||
type RenewEnrollmentProfileConfigFetcher struct {
|
||||
type renewEnrollmentProfileConfigFetcher struct {
|
||||
// Fetcher is the OrbitConfigFetcher that will be wrapped. It is responsible
|
||||
// for actually returning the orbit configuration or an error.
|
||||
Fetcher OrbitConfigFetcher
|
||||
@@ -36,13 +37,13 @@ type RenewEnrollmentProfileConfigFetcher struct {
|
||||
}
|
||||
|
||||
func ApplyRenewEnrollmentProfileConfigFetcherMiddleware(fetcher OrbitConfigFetcher, frequency time.Duration) OrbitConfigFetcher {
|
||||
return &RenewEnrollmentProfileConfigFetcher{Fetcher: fetcher, Frequency: frequency}
|
||||
return &renewEnrollmentProfileConfigFetcher{Fetcher: fetcher, Frequency: frequency}
|
||||
}
|
||||
|
||||
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
|
||||
// server set the renew enrollment profile flag to true, executes the command
|
||||
// to renew the enrollment profile.
|
||||
func (h *RenewEnrollmentProfileConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
|
||||
func (h *renewEnrollmentProfileConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
|
||||
cfg, err := h.Fetcher.GetConfig()
|
||||
|
||||
// TODO: download and use swiftDialog following the same patterns we
|
||||
@@ -83,3 +84,84 @@ func (h *RenewEnrollmentProfileConfigFetcher) GetConfig() (*fleet.OrbitConfig, e
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
type execWinAPIFunc func(MicrosoftMDMEnrollmentArgs) error
|
||||
|
||||
type microsoftMDMEnrollmentConfigFetcher struct {
|
||||
// Fetcher is the OrbitConfigFetcher that will be wrapped. It is responsible
|
||||
// for actually returning the orbit configuration or an error.
|
||||
Fetcher OrbitConfigFetcher
|
||||
// Frequency is the minimum amount of time that must pass between two
|
||||
// executions of the windows MDM enrollment attempt.
|
||||
Frequency time.Duration
|
||||
// HostUUID is the current host's UUID.
|
||||
HostUUID string
|
||||
|
||||
// for tests, to be able to mock command execution. If nil, will use
|
||||
// RunMicrosoftMDMEnrollment.
|
||||
execWinAPIFn execWinAPIFunc
|
||||
|
||||
// ensures only one command runs at a time, protects access to lastRun and
|
||||
// isWindowsServer.
|
||||
mu sync.Mutex
|
||||
lastRun time.Time
|
||||
isWindowsServer bool
|
||||
}
|
||||
|
||||
func ApplyMicrosoftMDMEnrollmentFetcherMiddleware(
|
||||
fetcher OrbitConfigFetcher,
|
||||
frequency time.Duration,
|
||||
hostUUID string,
|
||||
) OrbitConfigFetcher {
|
||||
return µsoftMDMEnrollmentConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: frequency,
|
||||
HostUUID: hostUUID,
|
||||
}
|
||||
}
|
||||
|
||||
var errIsWindowsServer = errors.New("device is a Windows Server")
|
||||
|
||||
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
|
||||
// server set the "needs windows enrollment" flag to true, executes the command
|
||||
// to enroll into Windows MDM (or not, if the device is a Windows Server).
|
||||
func (w *microsoftMDMEnrollmentConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
|
||||
cfg, err := w.Fetcher.GetConfig()
|
||||
|
||||
if err == nil && cfg.Notifications.NeedsProgrammaticMicrosoftMDMEnrollment {
|
||||
if cfg.Notifications.MicrosoftMDMDiscoveryEndpoint == "" {
|
||||
log.Info().Err(errors.New("discovery endpoint is missing")).Msg("skipping enrollment, discovery endpoint is empty")
|
||||
} else if w.mu.TryLock() {
|
||||
defer w.mu.Unlock()
|
||||
|
||||
// do not enroll Windows Servers, and do not attempt enrollment if the
|
||||
// last run is not at least Frequency ago.
|
||||
if !w.isWindowsServer && time.Since(w.lastRun) > w.Frequency {
|
||||
fn := w.execWinAPIFn
|
||||
if fn == nil {
|
||||
fn = RunMicrosoftMDMEnrollment
|
||||
}
|
||||
args := MicrosoftMDMEnrollmentArgs{
|
||||
DiscoveryURL: cfg.Notifications.MicrosoftMDMDiscoveryEndpoint,
|
||||
HostUUID: w.HostUUID,
|
||||
}
|
||||
if err := fn(args); err != nil {
|
||||
if errors.Is(err, errIsWindowsServer) {
|
||||
w.isWindowsServer = true
|
||||
log.Info().Msg("device is a Windows Server, skipping enrollment")
|
||||
} else {
|
||||
log.Info().Err(err).Msg("calling RegisterDeviceWithManagement to enroll Windows device failed")
|
||||
}
|
||||
} else {
|
||||
w.lastRun = time.Now()
|
||||
log.Info().Msg("successfully called RegisterDeviceWithManagement to enroll Windows device")
|
||||
}
|
||||
} else if w.isWindowsServer {
|
||||
log.Debug().Msg("skipped calling RegisterDeviceWithManagement to enroll Windows device, device is a server")
|
||||
} else {
|
||||
log.Debug().Msg("skipped calling RegisterDeviceWithManagement to enroll Windows device, last run was too recent")
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestRenewEnrollmentProfile(t *testing.T) {
|
||||
}
|
||||
|
||||
var cmdGotCalled bool
|
||||
renewFetcher := &RenewEnrollmentProfileConfigFetcher{
|
||||
renewFetcher := &renewEnrollmentProfileConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: time.Hour, // doesn't matter for this test
|
||||
runCmdFn: func() error {
|
||||
@@ -71,7 +71,7 @@ func TestRenewEnrollmentProfilePrevented(t *testing.T) {
|
||||
|
||||
var cmdCallCount int
|
||||
chProceed := make(chan struct{})
|
||||
renewFetcher := &RenewEnrollmentProfileConfigFetcher{
|
||||
renewFetcher := &renewEnrollmentProfileConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: 2 * time.Second, // just to be safe with slow environments (CI)
|
||||
runCmdFn: func() error {
|
||||
@@ -120,3 +120,140 @@ func TestRenewEnrollmentProfilePrevented(t *testing.T) {
|
||||
|
||||
require.Equal(t, 2, cmdCallCount) // the initial call and the one after sleep
|
||||
}
|
||||
|
||||
func TestWindowsMDMEnrollment(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
oldLog := log.Logger
|
||||
log.Logger = log.Output(&logBuf)
|
||||
t.Cleanup(func() { log.Logger = oldLog })
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
enrollFlag bool
|
||||
discoveryURL string
|
||||
apiErr error
|
||||
wantAPICalled bool
|
||||
wantLog string
|
||||
}{
|
||||
{"enroll=false", false, "", nil, false, ""},
|
||||
{"enroll=true,discovery=''", true, "", nil, false, "discovery endpoint is empty"},
|
||||
{"enroll=true,discovery!='',success", true, "http://example.com", nil, true, "successfully called RegisterDeviceWithManagement"},
|
||||
{"enroll=true,discovery!='',fail", true, "http://example.com", io.ErrUnexpectedEOF, true, "enroll Windows device failed"},
|
||||
{"enroll=true,discovery!='',server", true, "http://example.com", errIsWindowsServer, true, "device is a Windows Server, skipping enrollment"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
logBuf.Reset()
|
||||
|
||||
fetcher := &dummyConfigFetcher{
|
||||
cfg: &fleet.OrbitConfig{Notifications: fleet.OrbitConfigNotifications{
|
||||
NeedsProgrammaticMicrosoftMDMEnrollment: c.enrollFlag,
|
||||
MicrosoftMDMDiscoveryEndpoint: c.discoveryURL,
|
||||
}},
|
||||
}
|
||||
|
||||
var apiGotCalled bool
|
||||
enrollFetcher := µsoftMDMEnrollmentConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: time.Hour, // doesn't matter for this test
|
||||
execWinAPIFn: func(args MicrosoftMDMEnrollmentArgs) error {
|
||||
apiGotCalled = true
|
||||
return c.apiErr
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := enrollFetcher.GetConfig()
|
||||
require.NoError(t, err) // the dummy fetcher never returns an error
|
||||
require.Equal(t, fetcher.cfg, cfg) // the enrollment wrapper properly returns the expected config
|
||||
|
||||
require.Equal(t, c.wantAPICalled, apiGotCalled)
|
||||
require.Contains(t, logBuf.String(), c.wantLog)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsMDMEnrollmentPrevented(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
oldLog := log.Logger
|
||||
log.Logger = log.Output(&logBuf)
|
||||
t.Cleanup(func() { log.Logger = oldLog })
|
||||
|
||||
fetcher := &dummyConfigFetcher{
|
||||
cfg: &fleet.OrbitConfig{Notifications: fleet.OrbitConfigNotifications{
|
||||
NeedsProgrammaticMicrosoftMDMEnrollment: true,
|
||||
MicrosoftMDMDiscoveryEndpoint: "http://example.com",
|
||||
}},
|
||||
}
|
||||
|
||||
var (
|
||||
apiCallCount int
|
||||
apiErr error
|
||||
)
|
||||
chProceed := make(chan struct{})
|
||||
enrollFetcher := µsoftMDMEnrollmentConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: 2 * time.Second, // just to be safe with slow environments (CI)
|
||||
execWinAPIFn: func(args MicrosoftMDMEnrollmentArgs) error {
|
||||
<-chProceed // will be unblocked only when allowed
|
||||
apiCallCount++ // no need for sync, single-threaded call of this func is guaranteed by the fetcher's mutex
|
||||
return apiErr
|
||||
},
|
||||
}
|
||||
|
||||
assertResult := func(cfg *fleet.OrbitConfig, err error) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fetcher.cfg, cfg)
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
go func() {
|
||||
close(started)
|
||||
|
||||
// the first call will block in execWinAPIFn
|
||||
cfg, err := enrollFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
}()
|
||||
|
||||
<-started
|
||||
// this call will happen while the first call is blocked in execWinAPIFn, so it
|
||||
// won't call the API (won't be able to lock the mutex). However it will
|
||||
// still complete successfully without being blocked by the other call in
|
||||
// progress.
|
||||
cfg, err := enrollFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
// unblock the first call and wait for it to complete
|
||||
close(chProceed)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// this next call won't execute the command because of the frequency
|
||||
// restriction (it got called less than N seconds ago)
|
||||
cfg, err = enrollFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
// wait for the fetcher's frequency to pass
|
||||
time.Sleep(enrollFetcher.Frequency)
|
||||
|
||||
// this call executes the command, and it returns the Is Windows Server error
|
||||
apiErr = errIsWindowsServer
|
||||
cfg, err = enrollFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
// this next call won't execute the command (both due to frequency and the
|
||||
// detection of windows server)
|
||||
cfg, err = enrollFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
// wait for the fetcher's frequency to pass
|
||||
time.Sleep(enrollFetcher.Frequency)
|
||||
|
||||
// this next call still won't execute the command (due to the detection of
|
||||
// windows server)
|
||||
cfg, err = enrollFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
require.Equal(t, 2, apiCallCount) // the initial call and the one that returned errIsWindowsServer after first sleep
|
||||
}
|
||||
|
||||
@@ -341,3 +341,20 @@ type SoapFault struct {
|
||||
Reason Reason `xml:"s:reason"`
|
||||
OriginalMessageType int `xml:"-"`
|
||||
}
|
||||
|
||||
// MicrosoftMDMAccessTokenPayload is the payload that gets encoded as JSON and
|
||||
// provided as opaque access token to the RegisterDeviceWithManagement API.
|
||||
type MicrosoftMDMAccessTokenPayload struct {
|
||||
// Type is the enrollment type, such as "programmatic".
|
||||
Type MicrosoftMDMEnrollmentType `json:"type"`
|
||||
Payload struct {
|
||||
HostUUID string `json:"host_uuid"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
type MicrosoftMDMEnrollmentType int
|
||||
|
||||
// List of supported Microsoft MDM enrollment types.
|
||||
const (
|
||||
MicrosoftMDMProgrammaticEnrollmentType MicrosoftMDMEnrollmentType = 1
|
||||
)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
discoveryURL = flag.String("discovery-url", "", "The Windows MDM discovery URL")
|
||||
hostUUID = flag.String("host-uuid", "", "The Host UUID")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
err := update.RunMicrosoftMDMEnrollment(update.MicrosoftMDMEnrollmentArgs{
|
||||
DiscoveryURL: *discoveryURL,
|
||||
HostUUID: *hostUUID,
|
||||
})
|
||||
fmt.Println(err)
|
||||
}
|
||||
Reference in New Issue
Block a user