Skip to content

Commit da56dae

Browse files
authored
⭐️ add support for reading microsoft 365 devices (#5406)
* ⭐️ add support for reading microsoft 365 devices * 🧹 update device implementation * 🧹 add documentation
1 parent 621d4df commit da56dae

File tree

8 files changed

+721
-6
lines changed

8 files changed

+721
-6
lines changed

providers/ms365/resources/applications.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ func (a *mqlMicrosoftApplication) owners() ([]interface{}, error) {
387387
if err != nil {
388388
return nil, err
389389
}
390-
mqlMicrsoftResource.index(newUserResource.(*mqlMicrosoftUser))
390+
mqlMicrsoftResource.indexUser(newUserResource.(*mqlMicrosoftUser))
391391
res = append(res, newUserResource)
392392
}
393393
return res, nil

providers/ms365/resources/devices.go

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package resources
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
abstractions "github.com/microsoft/kiota-abstractions-go"
12+
betamodels "github.com/microsoftgraph/msgraph-beta-sdk-go/models"
13+
"github.com/microsoftgraph/msgraph-beta-sdk-go/reports"
14+
"github.com/microsoftgraph/msgraph-sdk-go/devices"
15+
"github.com/microsoftgraph/msgraph-sdk-go/models"
16+
"github.com/rs/zerolog/log"
17+
"go.mondoo.com/cnquery/v11/llx"
18+
"go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin"
19+
"go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert"
20+
"go.mondoo.com/cnquery/v11/providers/ms365/connection"
21+
"go.mondoo.com/cnquery/v11/types"
22+
)
23+
24+
// see https://learn.microsoft.com/en-us/graph/api/resources/device?view=graph-rest-1.0
25+
var deviceSelectFields = []string{
26+
"id", "displayName", "deviceId", "deviceCategory", "enrollmentProfileName", "enrollmentType",
27+
"isCompliant", "isManaged", "manufacturer", "isRooted", "mdmAppId", "model", "operatingSystem",
28+
"operatingSystemVersion", "physicalIds", "registrationDateTime", "systemLabels", "trustType",
29+
}
30+
31+
func initMicrosoftDevices(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) {
32+
args["__id"] = newListResourceIdFromArguments("microsoft.devices", args)
33+
resource, err := runtime.CreateResource(runtime, "microsoft.devices", args)
34+
if err != nil {
35+
return args, nil, err
36+
}
37+
38+
return args, resource.(*mqlMicrosoftDevices), nil
39+
}
40+
41+
// list fetches devices from Entra ID and allows the user provide a filter to retrieve
42+
// a subset of devices
43+
//
44+
// Permissions: Device.Read.All
45+
// see https://learn.microsoft.com/en-us/graph/api/device-list?view=graph-rest-1.0&tabs=http
46+
func (a *mqlMicrosoftDevices) list() ([]interface{}, error) {
47+
conn := a.MqlRuntime.Connection.(*connection.Ms365Connection)
48+
graphClient, err := conn.GraphClient()
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
betaClient, err := conn.BetaGraphClient()
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
// Index of devices are stored inside the top level resource `microsoft`, just like
59+
// MFA response. Here we create or get the resource to access those internals
60+
mainResource, err := CreateResource(a.MqlRuntime, "microsoft", map[string]*llx.RawData{})
61+
if err != nil {
62+
return nil, err
63+
}
64+
microsoft := mainResource.(*mqlMicrosoft)
65+
66+
// fetch device data
67+
ctx := context.Background()
68+
top := int32(999)
69+
opts := &devices.DevicesRequestBuilderGetRequestConfiguration{
70+
QueryParameters: &devices.DevicesRequestBuilderGetQueryParameters{
71+
Select: deviceSelectFields,
72+
Top: &top,
73+
},
74+
}
75+
76+
if a.Search.State == plugin.StateIsSet || a.Filter.State == plugin.StateIsSet {
77+
// search and filter requires this header
78+
headers := abstractions.NewRequestHeaders()
79+
headers.Add("ConsistencyLevel", "eventual")
80+
opts.Headers = headers
81+
82+
if a.Search.State == plugin.StateIsSet {
83+
log.Debug().
84+
Str("search", a.Search.Data).
85+
Msg("microsoft.devices.list.search set")
86+
search, err := parseSearch(a.Search.Data)
87+
if err != nil {
88+
return nil, err
89+
}
90+
opts.QueryParameters.Search = &search
91+
}
92+
if a.Filter.State == plugin.StateIsSet {
93+
log.Debug().
94+
Str("filter", a.Filter.Data).
95+
Msg("microsoft.devices.list.filter set")
96+
opts.QueryParameters.Filter = &a.Filter.Data
97+
count := true
98+
opts.QueryParameters.Count = &count
99+
}
100+
}
101+
102+
resp, err := graphClient.Devices().Get(ctx, opts)
103+
if err != nil {
104+
return nil, transformError(err)
105+
}
106+
devices, err := iterate[*models.Device](ctx,
107+
resp,
108+
graphClient.GetAdapter(),
109+
devices.CreateDeltaGetResponseFromDiscriminatorValue,
110+
)
111+
if err != nil {
112+
return nil, transformError(err)
113+
}
114+
115+
detailsResp, err := betaClient.
116+
Reports().
117+
AuthenticationMethods().
118+
UserRegistrationDetails().
119+
Get(ctx,
120+
&reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetRequestConfiguration{
121+
QueryParameters: &reports.AuthenticationMethodsUserRegistrationDetailsRequestBuilderGetQueryParameters{
122+
Top: &top,
123+
},
124+
})
125+
// we do not want to fail the device fetching here, this likely means the tenant does not have the right license
126+
if err != nil {
127+
microsoft.mfaResp = mfaResp{err: err}
128+
} else {
129+
userRegistrationDetails, err := iterate[*betamodels.UserRegistrationDetails](ctx, detailsResp, betaClient.GetAdapter(), betamodels.CreateUserRegistrationDetailsCollectionResponseFromDiscriminatorValue)
130+
// we do not want to fail the device fetching here, this likely means the tenant does not have the right license
131+
if err != nil {
132+
microsoft.mfaResp = mfaResp{err: err}
133+
} else {
134+
mfaMap := map[string]bool{}
135+
for _, u := range userRegistrationDetails {
136+
if u.GetId() == nil || u.GetIsMfaRegistered() == nil {
137+
continue
138+
}
139+
mfaMap[*u.GetId()] = *u.GetIsMfaRegistered()
140+
}
141+
microsoft.mfaResp = mfaResp{mfaMap: mfaMap}
142+
}
143+
}
144+
145+
// construct the result
146+
res := []interface{}{}
147+
for _, u := range devices {
148+
graphDevice, err := newMqlMicrosoftDevice(a.MqlRuntime, u)
149+
if err != nil {
150+
return nil, err
151+
}
152+
// indexUser devices by id
153+
microsoft.indexDevice(graphDevice)
154+
res = append(res, graphDevice)
155+
}
156+
157+
return res, nil
158+
}
159+
160+
func initMicrosoftDevice(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) {
161+
// we only look up the user if we have been supplied by id, displayName or userPrincipalName
162+
if len(args) > 1 {
163+
return args, nil, nil
164+
}
165+
166+
rawId, okId := args["id"]
167+
rawDisplayName, okDisplayName := args["displayName"]
168+
169+
if !okId && !okDisplayName {
170+
// required parameters are not set, we just pass-through the initialization arguments
171+
return args, nil, nil
172+
}
173+
174+
var filter *string
175+
if okId {
176+
idFilter := fmt.Sprintf("id eq '%s'", rawId.Value.(string))
177+
filter = &idFilter
178+
} else if okDisplayName {
179+
displayNameFilter := fmt.Sprintf("displayName eq '%s'", rawDisplayName.Value.(string))
180+
filter = &displayNameFilter
181+
}
182+
if filter == nil {
183+
return nil, nil, errors.New("no filter found")
184+
}
185+
186+
conn := runtime.Connection.(*connection.Ms365Connection)
187+
graphClient, err := conn.GraphClient()
188+
if err != nil {
189+
return nil, nil, err
190+
}
191+
192+
ctx := context.Background()
193+
resp, err := graphClient.Devices().Get(ctx, &devices.DevicesRequestBuilderGetRequestConfiguration{
194+
QueryParameters: &devices.DevicesRequestBuilderGetQueryParameters{
195+
Filter: filter,
196+
},
197+
})
198+
if err != nil {
199+
return nil, nil, transformError(err)
200+
}
201+
202+
val := resp.GetValue()
203+
if len(val) == 0 {
204+
return nil, nil, errors.New("device not found")
205+
}
206+
207+
deviceId := val[0].GetId()
208+
if deviceId == nil {
209+
return nil, nil, errors.New("device id not found")
210+
}
211+
212+
// fetch devices by id
213+
device, err := graphClient.Devices().ByDeviceId(*deviceId).Get(ctx, &devices.DeviceItemRequestBuilderGetRequestConfiguration{})
214+
if err != nil {
215+
return nil, nil, transformError(err)
216+
}
217+
mqlMsApp, err := newMqlMicrosoftDevice(runtime, device)
218+
if err != nil {
219+
return nil, nil, err
220+
}
221+
222+
return nil, mqlMsApp, nil
223+
}
224+
225+
func newMqlMicrosoftDevice(runtime *plugin.Runtime, u models.Deviceable) (*mqlMicrosoftDevice, error) {
226+
graphDevice, err := CreateResource(runtime, "microsoft.device",
227+
map[string]*llx.RawData{
228+
"__id": llx.StringDataPtr(u.GetId()),
229+
"id": llx.StringDataPtr(u.GetId()),
230+
"displayName": llx.StringDataPtr(u.GetDisplayName()),
231+
"deviceId": llx.StringDataPtr(u.GetDeviceId()),
232+
"deviceCategory": llx.StringDataPtr(u.GetDeviceCategory()),
233+
"enrollmentProfileName": llx.StringDataPtr(u.GetEnrollmentProfileName()),
234+
"enrollmentType": llx.StringDataPtr(u.GetEnrollmentType()),
235+
"isCompliant": llx.BoolDataPtr(u.GetIsCompliant()),
236+
"isManaged": llx.BoolDataPtr(u.GetIsManaged()),
237+
"manufacturer": llx.StringDataPtr(u.GetManufacturer()),
238+
"isRooted": llx.BoolDataPtr(u.GetIsRooted()),
239+
"mdmAppId": llx.StringDataPtr(u.GetMdmAppId()),
240+
"model": llx.StringDataPtr(u.GetModel()),
241+
"operatingSystem": llx.StringDataPtr(u.GetOperatingSystem()),
242+
"operatingSystemVersion": llx.StringDataPtr(u.GetOperatingSystemVersion()),
243+
"physicalIds": llx.ArrayData(convert.SliceAnyToInterface(u.GetPhysicalIds()), types.String),
244+
"registrationDateTime": llx.TimeDataPtr(u.GetRegistrationDateTime()),
245+
"systemLabels": llx.ArrayData(convert.SliceAnyToInterface(u.GetSystemLabels()), types.String),
246+
"trustType": llx.StringDataPtr(u.GetTrustType()),
247+
})
248+
if err != nil {
249+
return nil, err
250+
}
251+
return graphDevice.(*mqlMicrosoftDevice), nil
252+
}

providers/ms365/resources/groups.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func (a *mqlMicrosoftGroup) members() ([]interface{}, error) {
7474
if err != nil {
7575
return nil, err
7676
}
77-
mqlMicrosoftResource.index(newUserResource.(*mqlMicrosoftUser))
77+
mqlMicrosoftResource.indexUser(newUserResource.(*mqlMicrosoftUser))
7878
res = append(res, newUserResource)
7979
}
8080
return res, nil

providers/ms365/resources/microsoft.go

+29-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
)
99

1010
var idxUsersById = &sync.RWMutex{}
11+
var idxDevicesById = &sync.RWMutex{}
1112

1213
type mfaResp struct {
1314
// holds the error if that is what the request returned
@@ -19,6 +20,8 @@ type mqlMicrosoftInternal struct {
1920
permissionIndexer
2021
// index users by id
2122
idxUsersById map[string]*mqlMicrosoftUser
23+
// index devices by id
24+
idxDevicesById map[string]*mqlMicrosoftDevice
2225
// the response when asking for the user registration details
2326
mfaResp mfaResp
2427
}
@@ -29,17 +32,20 @@ func (a *mqlMicrosoft) initIndex() {
2932
if a.idxUsersById == nil {
3033
a.idxUsersById = make(map[string]*mqlMicrosoftUser)
3134
}
35+
if a.idxDevicesById == nil {
36+
a.idxDevicesById = make(map[string]*mqlMicrosoftDevice)
37+
}
3238
}
3339

34-
// index adds a user to the internal indexes
35-
func (a *mqlMicrosoft) index(user *mqlMicrosoftUser) {
40+
// indexUser adds a user to the internal indexes
41+
func (a *mqlMicrosoft) indexUser(user *mqlMicrosoftUser) {
3642
a.initIndex()
3743
idxUsersById.Lock()
3844
a.idxUsersById[user.Id.Data] = user
3945
idxUsersById.Unlock()
4046
}
4147

42-
// userById returns a user by id if it exists in the index
48+
// userById returns a user by id if it exists in the indexUser
4349
func (a *mqlMicrosoft) userById(id string) (*mqlMicrosoftUser, bool) {
4450
if a.idxUsersById == nil {
4551
return nil, false
@@ -50,3 +56,23 @@ func (a *mqlMicrosoft) userById(id string) (*mqlMicrosoftUser, bool) {
5056
idxUsersById.RUnlock()
5157
return res, ok
5258
}
59+
60+
// indexDevice adds a device to the internal indexes
61+
func (a *mqlMicrosoft) indexDevice(device *mqlMicrosoftDevice) {
62+
a.initIndex()
63+
idxUsersById.Lock()
64+
a.idxDevicesById[device.Id.Data] = device
65+
idxUsersById.Unlock()
66+
}
67+
68+
// deviceById returns a device by id if it exists in the indexDevice
69+
func (a *mqlMicrosoft) deviceById(id string) (*mqlMicrosoftDevice, bool) {
70+
if a.idxDevicesById == nil {
71+
return nil, false
72+
}
73+
74+
idxDevicesById.RLock()
75+
res, ok := a.idxDevicesById[id]
76+
idxDevicesById.RUnlock()
77+
return res, ok
78+
}

providers/ms365/resources/ms365.lr

+51
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,57 @@ private microsoft.group @defaults("id displayName") {
263263
membershipRuleProcessingState string
264264
}
265265

266+
// List of Microsoft Entra devices
267+
microsoft.devices {
268+
[]microsoft.device
269+
270+
init(filter? string, search? string)
271+
// Filter devices by property values
272+
filter string
273+
// Search devices by search phrases
274+
search string
275+
}
276+
277+
// Microsoft device
278+
private microsoft.device @defaults("id displayName") {
279+
// Device ID
280+
id string
281+
// Device display name
282+
displayName string
283+
// Unique identifier set
284+
deviceId string
285+
// User-defined property set by Intune
286+
deviceCategory string
287+
// Enrollment profile applied to the device
288+
enrollmentProfileName string
289+
// Enrollment type of the device
290+
enrollmentType string
291+
// Whether the device complies with Mobile Device Management (MDM) policies
292+
isCompliant bool
293+
// Whether the device is managed by a Mobile Device Management (MDM) app
294+
isManaged bool
295+
// Manufacturer
296+
manufacturer string
297+
// Whether the device is rooted or jail-broken
298+
isRooted bool
299+
// Application identifier used to register device into MDM
300+
mdmAppId string
301+
// Model of the device
302+
model string
303+
// The type of operating system on the device
304+
operatingSystem string
305+
// The version of the operating system on the device
306+
operatingSystemVersion string
307+
// Physical IDs
308+
physicalIds []string
309+
// Date and time of when the device was registered
310+
registrationDateTime time
311+
// List of labels applied to the device by the system
312+
systemLabels []string
313+
// Type of trust for the joined device
314+
trustType string
315+
}
316+
266317
// Microsoft domain
267318
private microsoft.domain @defaults("id") {
268319
// Domain ID

0 commit comments

Comments
 (0)